Как десериализовать JPS свойство обернутой коллекции в универсальный класс? - PullRequest
2 голосов
/ 27 сентября 2019

Проблема

Многие API-интерфейсы RESTful JSON возвращают коллекции, обернутые в объект .
В этом случае вместе с другим свойством, которое содержит количество элементов в коллекции.

Два примера:

{
    "animals": [
        {"name": "Raven", "wings": 2},
        {"name": "Wolf", "wings": 0}
    ],
    "count": 2
}
{
    "vehicles": [
        {"type": "Car", "wheels": 4},
        {"type": "Motorcycle", "wheels": 2}
        {"type": "Boat", "wheels": 0}
    ],
    "count": 3
}

Обратите внимание, что эти корневые объекты не являются одинаковыми, поскольку они содержат имя коллекции .

Это не проблема в слабо типизированном языке, таком как JavaScript, но в строго типизированных языках это становится проблемой, если существует много подобных объектов-оболочек.

Безгенерики

Конечно, их можно десериализовать так:

class AnimalsResponse 
{
    public List<Animal> Animals { get; set; }
    public int Count { get; set; }
}

class Animal
{
    public string Name { get; set; }
    public int Wings { get; set; }
}

JsonConvert.DeserializeObject<AnimalsResponse>(content);
class VehiclesResponse 
{
    public List<Vehicle> Vehicles { get; set; }
    public int Count { get; set; }
} 

class Vehicle
{
    public string Type { get; set; }
    public int Wheels { get; set; }
}

JsonConvert.DeserializeObject<VehiclesResponse>(content);

с генериками

Поскольку корневой объектвсегда в одном и том же формате, за исключением названия коллекции, было бы желательно десериализовать в универсальный тип:

class CollectionResponse<T>
{
    public List<T> Items { get; set; }
    public int Count { get; set; }
}

(возможно, слишком сложное) решение

Я уже пробовал (успешно) сделать это с помощью пользовательского десериализатора, но это решение кажется слишком сложным для такой распространенной проблемы.

class CollectionJsonConverter : JsonConverter
{
    private readonly string collectionName;

    public CollectionJsonConverter(string collectionName) : base()
    {
        this.collectionName = collectionName ?? throw new ArgumentNullException(nameof(collectionName));
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType.IsGenericType && 
            objectType.GetGenericTypeDefinition() == typeof(CollectionResponse<>);
    }

    public override object ReadJson(JsonReader reader, 
        Type objectType, object existingValue, JsonSerializer serializer)
    {
        var instance = Activator.CreateInstance(objectType);

        var objectProperties = objectType.GetTypeInfo().DeclaredProperties.ToList();
        var objectProperty = objectProperties.FirstOrDefault(pi =>
            pi.Name == nameof(CollectionResponse<object>.Items));

        var jsonProperties = JObject.Load(reader).Properties();
        var jsonProperty = jsonProperties.FirstOrDefault(p => p.Name == collectionName);

        objectProperty?.SetValue(instance, 
            jsonProperty.Value.ToObject(objectProperty.PropertyType, new JsonSerializer()));

        return instance;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

JsonConvert.DeserializeObject<CollectionResponse<T>>(content, new CollectionJsonConverter(collectionName));

Где collectionName - имя коллекции (например, "animals" или "vehicles" вприведенные выше примеры).
К сожалению, к этому добавляется дополнительный параметр.

Вопрос

Есть ли более простой способ добиться этого, желательно без специального пользователя?ializer

1 Ответ

0 голосов
/ 27 сентября 2019

Нет способа сделать то, что вы просите, без пользовательского JsonConverter или пользовательского ContractResolver.Я думаю, что JsonConverter на самом деле является хорошим подходом, но вы можете немного его упростить, чтобы вам не нужно было передавать имя коллекции и вам не нужно было размышлять, чтобы установить свойство Items:

class CollectionJsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType.IsGenericType &&
            objectType.GetGenericTypeDefinition() == typeof(CollectionResponse<>);
    }

    public override object ReadJson(JsonReader reader,
        Type objectType, object existingValue, JsonSerializer serializer)
    {
        // load the JSON into a JObject
        var obj = JObject.Load(reader);

        // we expect one and only one list of items; don't care what its name is
        var itemsProp = obj.Properties().Single(p => p.Value.Type == JTokenType.Array);

        // replace the existing list property with a new one called "Items"
        itemsProp.Replace(new JProperty("Items", itemsProp.Value));

        // create an instance of the CollectionResponse model
        var instance = Activator.CreateInstance(objectType);

        // populate it from the modified JObject
        serializer.Populate(obj.CreateReader(), instance);

        return instance;
    }

    public override bool CanWrite => false;

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Кроме того, вы можете украсить свой класс CollectionResponse<T> атрибутом [JsonConverter], чтобы связать преобразователь с классом, чтобы вам никогда не приходилось беспокоиться о передаче преобразователя при десериализации:

[JsonConverter(typeof(CollectionJsonConverter))]
class CollectionResponse<T>
{
    public List<T> Items { get; set; }
    public int Count { get; set; }
}

Тогда вы можете просто десериализовать вот так (например):

var vehiclesResp = JsonConvert.DeserializeObject<CollectionResponse<Vehicle>>(vehiclesJson);

Рабочая демонстрация: https://dotnetfiddle.net/D3q8ub

...