Десериализация Json в С#

Рейтинг: 1Ответов: 2Опубликовано: 19.08.2023

От API мне приходит json файл следующего вида:

{
  "location": {
    "name": "name",
    "region": "region",
    "country": "Russia",
    "lat": xx.xx,
    "lon": yy.yy,
    "tz_id": "xx/yy",
    "localtime_epoch": 11111111,
    "localtime": "2023-08-18 23:38"
  },
  "current": {
    "last_updated_epoch": 1692390600,
    "last_updated": "2023-08-18 23:30",
    "temp_c": 21.0,
    "is_day": 0,
    "condition": {
      "text": "Ясно",
      "icon": "//cdn.weatherapi.com/weather/64x64/night/116.png",
      "code": 1003
    },
    "wind_kph": 15.1,
    "wind_degree": 20,
    "wind_dir": "NNE",
    "pressure_mb": 1025.0,
    "pressure_in": 30.27,
    "precip_mm": 0.8,
    "humidity": 78,
    "cloud": 50,
    "feelslike_c": 21.0,
    "vis_km": 10.0,
    "uv": 1.0,
    "gust_kph": 21.6
  }
}

Так же у меня есть класс Weather со следующими полями:

    public string LastUpdate;
    public double TemperatureCelsius; // в json: current -> temp_c
    public double FeelsLikeCelsius;
    public string Discription; // в json: current -> condition -> text
    public double WindSpeed;
    public string WindDirection;
    public double Pressure;
    public double Precipitation;
    public int Humidity;
    public int Cloud;
    public int UvIndex;
    public bool IsDay;

Таким образом, просто JsonConvert.DeserializeObject<Weather> не работает. Конечно, я могу десериализовать в словарь с множеством вложенных словарей, но этот вариант не очень читаемый и поэтому отталкивает меня. Как я могу получить данные из вложений файла (например из current -> condition взять значение text) более элегантным способом, при этом не создавая в классе ненужные мне поля?

Ответы

▲ 3Принят

Если проблема в привязки определённых полей объекта к определённым полям json:

В случае, если вы хотите вместо простого имени поля указать путь до этого самого поля, то это решение для вас:

Решение для System.Text.Json:

Я долго думал и всё же сделал довольно простое решение для стандартной библиотеки:

public class PathJsonConverter<T> : JsonConverter<T> where T : new()
{
    // Этот метод упрощает создание настроек
    public static T? Deserialize(string json, JsonNamingPolicy? namingPolicy = null) 
    {
        var options = new JsonSerializerOptions();
        options.PropertyNamingPolicy = namingPolicy;
        options.Converters.Add(new PathJsonConverter<T>());
        return JsonSerializer.Deserialize<T>(json, options);
    }

    public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var jsonObject = JsonNode.Parse(ref reader);
        
        T? result = new T();

        foreach (var property in typeToConvert.GetProperties())
        {
            JsonNode? currentNode = jsonObject;
            var attr = property.GetCustomAttribute<JsonPropertyNameAttribute>();
            if (attr == null)
                currentNode = currentNode?[options.PropertyNamingPolicy?.ConvertName(property.Name) ?? property.Name];
            else
                foreach (var path in attr.Name.Split('.'))
                    currentNode = currentNode?[path];

            if (currentNode != null)
                property.SetValue(result, getObject(currentNode));
        }

        return result;
    }
    object? getObject(JsonNode node) => node.GetValue<JsonElement>().ValueKind switch
    {
        JsonValueKind.Null => null,
        JsonValueKind.Undefined => null,
        JsonValueKind.String => node.GetValue<string>(),
        JsonValueKind.Array => node.GetValue<JsonArray>(),
        JsonValueKind.Object => node.GetValue<JsonObject>(),
        JsonValueKind.Number => node.AsValue().TryGetValue(out int val) ? (object)val : (node.AsValue().TryGetValue(out float fval) ? (object)fval : (object)node.GetValue<double>()),
        JsonValueKind.False => false,
        JsonValueKind.True => true,
        _ => null
    };

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) =>
        throw new NotImplementedException();
}

Данный способ позволяет указывать json путь в JsonPropertyName, а так же позволяет использовать имя поля когда путь не указан и поддерживает кастомный PropertyNamingPolicy (из решения ниже)

Использование:

class Model
{
    [JsonPropertyName("values.val")]
    public int ValuesVal { get; set; }

    [JsonPropertyName("single")]
    public int Value { get; set; }

    public string? OtherValue { get; set; }
}

string json = @"{""values"":{""val"":4}, ""single"": 2, ""other_value"": ""Here is other value""}";

// SnakeCaseNamingPolicy взят из решения  
// проблемы с политикой наименования ниже
// Данный параметр не обязателен, и нужен только
// для преобразования из "OtherValue" в "other_value"
Model? model = PathJsonConverter<Model>.Deserialize(json, new SnakeCaseNamingPolicy());

Console.WriteLine($"{model?.Value}, {model?.ValuesVal}, {model?.OtherValue}");

Решение для Newtonsoft.Json:

Я рекомендую вам использовать Newtonsoft.Json, так как он представляет очень мощный функционал для работы с json и лучше кастомизируется.

class JsonPathConverter : JsonConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
    {
        JObject jsonObj = JObject.Load(reader);
        object targetObj = Activator.CreateInstance(objectType) ?? throw new JsonException($"Can't create instance of {objectType}");

        foreach (PropertyInfo prop in objectType.GetProperties().Where(p => p.CanRead && p.CanWrite))
        {
            JsonPropertyAttribute? att = prop.GetCustomAttributes(true).OfType<JsonPropertyAttribute>().FirstOrDefault();
            string jsonPath = att?.PropertyName ?? prop.Name;
            JToken? token = jsonObj.SelectToken(jsonPath);

            if ((token?.Type ?? JTokenType.Null) != JTokenType.Null)
            {
                object? value = token?.ToObject(prop.PropertyType, serializer);
                prop.SetValue(targetObj, value, null);
            }
        }
        return targetObj;
    }

    public override bool CanConvert(Type objectType) => false;
    public override bool CanWrite => false;
    public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotImplementedException();
}

Использование:

[JsonConverter(typeof(JsonPathConverter))]
class TestClass
{
    [JsonProperty("values.some_value")]
    public int SomeValue { get; set; }
}

TestClass testClass = JsonConvert.DeserializeObject<TestClass>(@"{""values"": {""some_value"": 5}}");

Console.WriteLine(testClass.SomeValue)

Если проблема в политики наименования:

Если же проблема в соответствии способов наименования (типо ObjectName и object_name), то вам сюда

Решение для System.Net.Json:

public class SnakeCaseNamingPolicy : JsonNamingPolicy
{
    public override string ConvertName(string name) => 
        string.Concat(name.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString())).ToLower();
}

Использование:

TestObject testObject = JsonSerializer.Deserialize<TestObject>(
    "{\"int_value\": 5}", 
    new JsonSerializerOptions()
    {
        PropertyNamingPolicy = new SnakeCaseNamingPolicy()
    }
);
Console.WriteLine(testObject.IntValue);

Решение для Newtonsoft.Json:

Решение для данной задачи уже была встроена в библиотеку:

TestObject testObject = JsonConvert.DeserializeObject<TestObject>(
    "{\"int_value\": 5}",
    new JsonSerializerSettings()
    {
        ContractResolver = new DefaultContractResolver()
        { NamingStrategy = new SnakeCaseNamingStrategy() }
    }
);
Console.WriteLine(testObject.IntValue);
▲ -1

Нашел возможное решение - использовать тип dynamic. Да, им лучше не злоупотреблять, так как сложнее отлавливать состояния, но конкретно в моем случае решение неплохое.

        dynamic weatherData = JsonConvert.DeserializeObject(jsonData);
        weatherData = weatherData["current"];

        LastUpdate = weatherData["last_updated"];
        TemperatureCelsius = weatherData["temp_c"];
        FeelsLikeCelsius = weatherData["feelslike_c"];
        Discription = weatherData["condition"]["text"];
        WindSpeed = weatherData["wind_kph"];
        WindDirection = weatherData["wind_dir"];
        Pressure = weatherData["pressure_mb"];
        Precipitation = weatherData["precip_mm"];
        Humidity = weatherData["humidity"];
        Cloud = weatherData["cloud"];
        UvIndex = weatherData["uv"];
        IsDay = weatherData["is_day"];

Изменено: в результате я отказался от такого решения и, как было предложено в комментариях, создал несколько вспомогательных классов.