2014-01-02 3 views
10

Я программирую против стороннего API, который возвращает данные JSON, но формат может быть немного странным. Некоторые свойства могут быть либо объектом (который содержит свойство Id), либо строкой (которая является идентификатором объекта). Например, оба следующих справедливы:Deserialize JSON для нескольких свойств

{ 
    ChildObject: 'childobjectkey1' 
} 

и

{ 
    ChildObject: { 
     Id: 'childobjectkey1', 
     // (other properties) 
    } 
} 

Я пытаюсь десериализации это с помощью JSON.net в строго типизированного класса, но не везло так далеко. Моя лучшая идея состояла в том, чтобы сериализация его к двум свойствам, одной строки, а другому объекту, и использовать пользовательские JsonConverter для каждого, чтобы позволить переменному поведение:

public abstract class BaseEntity 
{ 
    public string Id { get; set; } 
} 

public class ChildObject : BaseEntity { } 

public class MyObject 
{ 
    [JsonProperty("ChildObject")] 
    [JsonConverter(typeof(MyCustomIdConverter))] 
    public string ChildObjectId { get; set; } 

    [JsonProperty("ChildObject")] 
    [JsonConverter(typeof(MyCustomObjectConverter))] 
    public ChildObject ChildObject { get; set; } 
} 

Однако, установку атрибута на два JsonProperty свойства с тем же PropertyName вызывает исключение:

Newtonsoft.Json.JsonSerializationException: член с именем «ChildObject» уже существует на «.....». Используйте JsonPropertyAttribute, чтобы указать другое имя.

Я довольно уверен, что подход JsonConverter будет работать, если я могу получить за это препятствие - я подозреваю, что ошибка есть, поскольку атрибут JsonProperty используется для сериализации, а также десериализации. В этом случае я не заинтересован в сериализации этого класса - он будет использоваться только как цель для десериализации.

У меня нет контроля над удаленным концом (это сторонний API), но я хотел бы иметь возможность добиться этой сериализации. Я не против, если он использует подход, который я начал, или тот, о котором я еще не думал.

This question также имеет отношение, но ответов не было.

+1

Я думаю, что конвертер должен быть для целого объекта, а не для отдельного поля ... (формат BTW кажется довольно разумным для сложного графика сущностей) –

+0

В надежде избежать пользовательского конвертера для всего объекта, так как существует несколько экземпляров это, и некоторые типы имеют более одного свойства, подобного этому. – Richard

ответ

6

Попробуйте это (продлить его с некоторой тщательной проверки, если вы будете использовать его в коде):

public class MyObject 
{ 
    public ChildObject MyChildObject; 
    public string MyChildObjectId; 

    [JsonProperty("ChildObject")] 
    public object ChildObject 
    { 
     get 
     { 
      return MyChildObject; 
     } 
     set 
     { 
      if (value is JObject) 
      { 
       MyChildObject = ((JToken)value).ToObject<ChildObject>(); 
       MyChildObjectId = MyChildObject.Id; 
      } 
      else 
      { 
       MyChildObjectId = value.ToString(); 
       MyChildObject = null; 
      } 
     } 
    } 
} 
+0

Интересная идея; единственная проблема здесь в том, что свойство 'ChildObject' больше не строго типизировано, поэтому вам нужно будет соответствующим образом применить его при каждом использовании. –

+1

Интересно, не думал о получении 'JObject' напрямую. В то время как я обычно не хотел бы разделить логику, так что некоторые из них находятся в DTO, это становится самым опрятным решением из-за пределов моей сборки, поскольку свойство, типичное для объекта, также может быть сделано внутренним. Благодаря! – Richard

+1

Это отлично сработало для меня :) Спасибо! – Tony

3

Вместо создания двух отдельных преобразователей для каждого из полей, было бы целесообразно создать один конвертер для «основного» свойства и связать его с другим. ChildObjectId является производным от ChildObject.

public class MyObject 
{ 
    [JsonIgnore] 
    public string ChildObjectId 
    { 
     get { return ChildObject.Id; } 

     // I would advise against having a setter here 
     // you should only allow changes through the object only 
     set { ChildObject.Id = value; } 
    } 

    [JsonConverter(typeof(MyObjectChildObjectConverter))] 
    public ChildObject ChildObject { get; set; } 
} 

Теперь для преобразования ChildObject может быть немного сложной задачей. Существует два возможных представления объекта: строка или объект. Определите, какое представление у вас есть, и выполните преобразование.

public class MyObjectChildObjectConverter : JsonConverter 
{ 
    public override bool CanConvert(Type objectType) 
    { 
     return objectType == typeof(ChildObject); 
    } 

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 
    { 
     serializer.Serialize(writer, value); 
    } 

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 
    { 
     var obj = serializer.Deserialize<JToken>(reader); 
     switch (obj.Type) 
     { 
     case JTokenType.Object: 
      return ReadAsObject(obj as JObject); 
     case JTokenType.String: 
      return ReadAsString((string)(JValue)obj); 
     default: 
      throw new JsonSerializationException("Unexpected token type"); 
     } 
    } 

    private object ReadAsObject(JObject obj) 
    { 
     return obj.ToObject<ChildObject>(); 
    } 

    private object ReadAsString(string str) 
    { 
     // do a lookup for the actual object or whatever here 
     return new ChildObject 
     { 
      Id = str, 
     }; 
    } 
} 
2

Вот что я хотел бы сделать в этой ситуации.

  • имеют только одно свойство в вашем родительском классе для дочернего объекта, и сделать его типа ChildObject
  • Создание пользовательского JsonConverter, которые могут инспектировать JSON и либо:
    • десериализации полный экземпляр дочерний объект, если данные присутствуют, или
    • создать новый экземпляр дочернего объекта и установить его идентификатор, оставив все остальные свойства пустыми. (Или вы могли бы сделать, как предложил Джефф Меркадо, и чтобы конвертер загружал объект из базы данных на основе идентификатора, если это относится к вашей ситуации.)
  • Возможно, поместите свойство на дочерний объект, указав, полностью заполнен. Преобразователь может установить это свойство во время десериализации.

После десериализации, если была ChildObject собственность в формате JSON (или с идентификатором или полной стоимости объекта), вы гарантированно иметь ChildObject экземпляр, и вы можете получить его идентификатор из него; в противном случае, если в JSON не было свойства ChildObject, свойство ChildObject в родительском классе будет равно NULL.

Ниже приведен полный рабочий пример для демонстрации. В этом примере я изменил родительский класс на три отдельных экземпляра ChildObject, чтобы показать различные возможности в JSON (только идентификатор строки, полный объект и ни один из них). Все они используют один и тот же конвертер. Я также добавил Name собственности и IsFullyPopulated собственности ChildObject класса.

Вот классы DTO:

public abstract class BaseEntity 
{ 
    public string Id { get; set; } 
} 

public class ChildObject : BaseEntity 
{ 
    public string Name { get; set; } 
    public bool IsFullyPopulated { get; set; } 
} 

public class MyObject 
{ 
    [JsonProperty("ChildObject1")] 
    [JsonConverter(typeof(MyCustomObjectConverter))] 
    public ChildObject ChildObject1 { get; set; } 

    [JsonProperty("ChildObject2")] 
    [JsonConverter(typeof(MyCustomObjectConverter))] 
    public ChildObject ChildObject2 { get; set; } 

    [JsonProperty("ChildObject3")] 
    [JsonConverter(typeof(MyCustomObjectConverter))] 
    public ChildObject ChildObject3 { get; set; } 
} 

Здесь вы нейтрализатор:

class MyCustomObjectConverter : JsonConverter 
{ 
    public override bool CanConvert(Type objectType) 
    { 
     return (objectType == typeof(ChildObject)); 
    } 

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 
    { 
     JToken token = JToken.Load(reader); 
     ChildObject child = null; 
     if (token.Type == JTokenType.String) 
     { 
      child = new ChildObject(); 
      child.Id = token.ToString(); 
      child.IsFullyPopulated = false; 
     } 
     else if (token.Type == JTokenType.Object) 
     { 
      child = token.ToObject<ChildObject>(); 
      child.IsFullyPopulated = true; 
     } 
     else if (token.Type != JTokenType.Null) 
     { 
      throw new JsonSerializationException("Unexpected token: " + token.Type); 
     } 
     return child; 
    } 

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 
    { 
     serializer.Serialize(writer, value); 
    } 
} 

Вот тестовая программа для демонстрации работы преобразователя:

class Program 
{ 
    static void Main(string[] args) 
    { 
     string json = @" 
     { 
      ""ChildObject1"": 
      { 
       ""Id"": ""key1"", 
       ""Name"": ""Foo Bar Baz"" 
      }, 
      ""ChildObject2"": ""key2"" 
     }"; 

     MyObject obj = JsonConvert.DeserializeObject<MyObject>(json); 

     DumpChildObject("ChildObject1", obj.ChildObject1); 
     DumpChildObject("ChildObject2", obj.ChildObject2); 
     DumpChildObject("ChildObject3", obj.ChildObject3); 
    } 

    static void DumpChildObject(string prop, ChildObject obj) 
    { 
     Console.WriteLine(prop); 
     if (obj != null) 
     { 
      Console.WriteLine(" Id: " + obj.Id); 
      Console.WriteLine(" Name: " + obj.Name); 
      Console.WriteLine(" IsFullyPopulated: " + obj.IsFullyPopulated); 
     } 
     else 
     { 
      Console.WriteLine(" (null)"); 
     } 
     Console.WriteLine(); 
    } 
} 

И здесь приведено вышеизложенное:

ChildObject1 
    Id: key1 
    Name: Foo Bar Baz 
    IsFullyPopulated: True 

ChildObject2 
    Id: key2 
    Name: 
    IsFullyPopulated: False 

ChildObject3 
    (null) 
+0

Я тоже направлялся в этом направлении, но пытался избежать идеи собственности IsFullyLoaded. Я пойду с решением Алекса, поскольку я могу скрыть все беспорядочные биты в одной сборке и показать, что я хочу извне. – Richard

+0

Это решение не зависит от наличия свойства IsFullyPopulated; вы можете просто удалить его, если он вам не нужен.Я добавил его только для того, чтобы показать, что это можно сделать, поскольку это позволяет легко определить, имеет ли дочерний объект все свои данные или только идентификатор. Но наличие двух свойств на родительском объекте также подходит для этого. В конечном счете, это ваше собственное решение о том, как вы хотите, чтобы ваш код работал, поэтому, если другое решение работает лучше для вас, я в полном порядке с этим! Приветствия. –

Смежные вопросы