Уничтожить JSON для нескольких свойств

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

{
    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 для двух свойств с тем же именем свойства вызывает исключение:

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

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

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

Этот вопрос также связан, но ответов не было.

Ответ 1

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

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;
            }
        }
    }
}

Ответ 2

Вместо создания двух отдельных преобразователей для каждого из полей было бы разумно создать единый конвертер для "основного" свойства и связать с ним другой. 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,
        };
    }
}

Ответ 3

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

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

После десериализации, если в JSON (с идентификатором или полным значением объекта) было свойство ChildObject), вы гарантированно имеете экземпляр 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)