Дерериализация json с неизвестными полями

У меня есть json файлы которые нужно дерериализировать. Проблема в том что в них есть постоянные поля( есть во всех) и есть поля которые меняются(могут быть а могут и не быть). Вот мой код:

Log MyLog = new Log();
      
        public  void json (string file)
        {
            if (file != null)
            { 
                MyLog = JsonSerializer.Deserialize<Log>(File.ReadAllText(file));
            } 
        }

public class Log
    {
        public DateTime Timestamp { get; set; }
        public string Level { get; set; }
        public string MessageTemplate { get; set; }
        public string Exception { get; set; }
      
    }

И как потом мне в ДатаГрид WPF вывести все поля в таблице? Заранее спасибо


Ответы (2 шт):

Автор решения: aepot

К примеру, у вас есть Json

{
  "field1": "111",
  "field2": 123
}

и такой

{
  "field1": "111",
  "field3": 555,
  "field4": true
}

Но на момент чтения данных, вы не знаете, что там внутри.

Это всего-лишь пример, таких полей может быть полсотни, и это объект с полями, а не массив.

Вариант 1. Обобщенный класс

public class MyJsonData
{
    [JsonPropertyName("field1")]
    public string Field1 { get; set; }
    [JsonPropertyName("field2")]
    public int Field2 { get; set; }
    [JsonPropertyName("field3")]
    public int Field3 { get; set; }
    [JsonPropertyName("field4")]
    public bool Field4 { get; set; }
}

Десереализация здесь простая

MyJsonData data = JsonSerializer.Deserialize<MyJsonData>(File.ReadAllText(file));
Debug.WriteLine(data.Field2);

Вариант 2. Словарь объектов

А есть случаи, когда в этот json могут добавляться незнакомые поля, которые не хочется хардкодить, и тип данных в будущих полях неизвестен. На помощь приходят словарть и конвертер.

При этом, с использованием словаря создание класса для этой части json'a не требуется, но потребуется конвертер, потому что для обобщения типов значений для каждого поля я буду использовать тип object, то есть словами дилетанта (то есть меня), способ обобщения значимых типов под названием boxing.

Конвертер у меня есть вот такой. Он поддерживает все основные типы чисел, bool и строки. Из коробки JsonSerializer такие преобразования делать не умеет, поэтому приходится выкручиваться.

Конвертер

public class TokenToObjectConverter : JsonConverter<object>
{
    public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.String)
        {
            string stringValue = reader.GetString();
            if (int.TryParse(stringValue, out int intVal)) return intVal;
            if (long.TryParse(stringValue, out long longVal)) return longVal;
            if (double.TryParse(stringValue, out double doubleVal)) return doubleVal;
            if (decimal.TryParse(stringValue, out decimal decimalVal)) return decimalVal;
            return stringValue;
        }
        else
        if (reader.TokenType == JsonTokenType.Number)
        {
            if (reader.TryGetInt32(out int intVal)) return intVal;
            if (reader.TryGetInt64(out long longVal)) return longVal;
            if (reader.TryGetDouble(out double doubleVal)) return doubleVal;
            if (reader.TryGetDecimal(out decimal decimalVal)) return decimalVal;
        }
        else
        if (reader.TokenType == JsonTokenType.False || reader.TokenType == JsonTokenType.True) return reader.GetBoolean();
        return null;
    }
    public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) => throw new NotImplementedException();
}

Здесь я сообщаю сериализатору, что если принимающий тип object, то json токен надо прогнать через этот конвертер.

На самом деле, я сам написал этот конвертер под свои нужды, быстро, и "на коленке". Вы можете его дописать, чтобы он поддерживал даты, или еще что-нибудь нужное.

Процесс десериализации будет немного сложнее.

JsonSerializerOptions options = new JsonSerializerOptions { ReadCommentHandling = JsonCommentHandling.Skip };
options.Converters.Add(new TokenToObjectConverter());
Dictionary<string, object> data = JsonSerializer.Deserialize<Dictionary<string, object>>(File.ReadAllText(file), options);

// определять тип значения, который вернул конвертер, можно с помощью паттерн-матчинга с присваиванием
if (data.TryGetValue("field2", out object val) && val is int number)
    Debug.WriteLine(number);

Кстати, если не подключать конвертер, а просто оставить все как есть, все равно можно будет получить эти данные в виде строк, вызывая object.ToString(), но внутри object при этом будет скрываться JsonToken, и его нельзя будет сматчить как string или int напрямую, а только с помощью методов класса JsonToken.


Но я в итоге советую там где возможно придерживаться первого варианта, а второй вариант оставить для сложных случаев, где есть линейная куча разнотипных значимых данных, и нет вложенных объектов.

А еще если вы просто хотите засунуть эти данные в интерфейс, и не особо разбираться, что именно за тип данных приехал из json'а, то второй способ вам тоже подойдет, потому что в интерфейсе везде, где можно - object.

→ Ссылка
Автор решения: Aziz Umarov

Извините, но это звучит как ересь. "постоянные поля( есть во всех) и есть поля которые меняются".

На самом деле у вас всегда один класс Log с разными версиями. И если вы придерживаетесь принципов SOLID то у вас не должно возникнуть проблем с десериализацией. Если же нет то советую переделать так что ваш последний Log отвечал этим принципам, а точнее в данном случае O - open closed. Ну и как частный случай это Вариант 1. Обобщенный класс предыдушего ответа

→ Ссылка