Як реалізувати користувацький JsonConverter в JSON.NET, щоб дезаріалізувати Список об'єктів базового класу?


301

Я намагаюся розширити приклад JSON.net, наведений тут http://james.newtonking.com/projects/json/help/CustomCreationConverter.html

У мене є ще один підклас, що походить від базового класу / інтерфейсу

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Employee : Person
{
    public string Department { get; set; }
    public string JobTitle { get; set; }
}

public class Artist : Person
{
    public string Skill { get; set; }
}

List<Person> people  = new List<Person>
{
    new Employee(),
    new Employee(),
    new Artist(),
};

Як дезаріалізувати, перейшовши на Json назад до списку <Особа>

[
  {
    "Department": "Department1",
    "JobTitle": "JobTitle1",
    "FirstName": "FirstName1",
    "LastName": "LastName1"
  },
  {
    "Department": "Department2",
    "JobTitle": "JobTitle2",
    "FirstName": "FirstName2",
    "LastName": "LastName2"
  },
  {
    "Skill": "Painter",
    "FirstName": "FirstName3",
    "LastName": "LastName3"
  }
]

Я не хочу використовувати TypeNameHandling JsonSerializerSettings. Я спеціально шукаю користувальницьку реалізацію JsonConverter для вирішення цього питання. Документація та приклади навколо цього є досить рідкими в мережі. Я, здається, не можу отримати переотриману методику ReadJson () в JsonConverter право.


Відповіді:


315

Використовуючи стандарт CustomCreationConverter, я намагався працювати над тим, як генерувати правильний тип ( Personабо Employee), тому що для того, щоб визначити це, потрібно проаналізувати JSON і не існує вбудованого способу зробити це за допомогою Createметоду.

Я знайшов дискусійну тему, що стосується перетворення типів, і виявилося, що вона дає відповідь. Ось посилання: Тип перетворення .

Що потрібно - це підклас JsonConverter, переопределення ReadJsonметоду та створення нового абстрактного Createметоду, який приймає a JObject.

Клас JObject надає засоби для завантаження об'єкта JSON та забезпечує доступ до даних усередині цього об’єкта.

Переопределений ReadJsonметод створює a JObjectі викликає Createметод (реалізований нашим похідним класом перетворювача), передаючи JObjectекземпляр.

Потім цей JObjectприклад може бути проаналізований для визначення правильного типу, перевіривши наявність певних полів.

Приклад

string json = "[{
        \"Department\": \"Department1\",
        \"JobTitle\": \"JobTitle1\",
        \"FirstName\": \"FirstName1\",
        \"LastName\": \"LastName1\"
    },{
        \"Department\": \"Department2\",
        \"JobTitle\": \"JobTitle2\",
        \"FirstName\": \"FirstName2\",
        \"LastName\": \"LastName2\"
    },
        {\"Skill\": \"Painter\",
        \"FirstName\": \"FirstName3\",
        \"LastName\": \"LastName3\"
    }]";

List<Person> persons = 
    JsonConvert.DeserializeObject<List<Person>>(json, new PersonConverter());

...

public class PersonConverter : JsonCreationConverter<Person>
{
    protected override Person Create(Type objectType, JObject jObject)
    {
        if (FieldExists("Skill", jObject))
        {
            return new Artist();
        }
        else if (FieldExists("Department", jObject))
        {
            return new Employee();
        }
        else
        {
            return new Person();
        }
    }

    private bool FieldExists(string fieldName, JObject jObject)
    {
        return jObject[fieldName] != null;
    }
}

public abstract class JsonCreationConverter<T> : JsonConverter
{
    /// <summary>
    /// Create an instance of objectType, based properties in the JSON object
    /// </summary>
    /// <param name="objectType">type of object expected</param>
    /// <param name="jObject">
    /// contents of JSON object that will be deserialized
    /// </param>
    /// <returns></returns>
    protected abstract T Create(Type objectType, JObject jObject);

    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }

    public override bool CanWrite
    {
        get { return false; }
    }

    public override object ReadJson(JsonReader reader, 
                                    Type objectType, 
                                     object existingValue, 
                                     JsonSerializer serializer)
    {
        // Load JObject from stream
        JObject jObject = JObject.Load(reader);

        // Create target object based on JObject
        T target = Create(objectType, jObject);

        // Populate the object properties
        serializer.Populate(jObject.CreateReader(), target);

        return target;
    }
}

6
Було б непогано також застосувати метод WriteJson і надати деякий абстрактний метод для строфікації типу.
Трайнко

54
ПРИМІТКА. Це рішення є в Інтернеті, але воно має вади, які виявляються в рідкісних випадках. Новий JsonReaderстворений в ReadJsonметоді не буде наслідувати які - небудь з параметрів конфігурації вихідного читача ( Culture, DateParseHandling, DateTimeZoneHandling, FloatParseHandlingі т.д. ...). Ці значення слід скопіювати, перш ніж використовувати новий JsonReaderв serializer.Populate().
Ален

9
Щоб запобігти створенню нового JsonReader (через причини, згадані @Alain), або якщо вам потрібно визначитися з типом об'єкта, створеного виходячи з певного значення батьків, див. Це рішення stackoverflow.com/a/22539730/1038496 . Мені здається більш ефективною і зрозумілішою (навіть для подібного роду проблем).
Зока

8
@Triynko: Після тривалого пошуку я виявив, що клас JsonConverterмає властивість називатися CanReadі CanWrite. Якщо вам не потрібна спеціальна WriteJsonреалізація, достатньо дозволити CanWriteповернутися FALSE. Потім система повернеться до стандартної поведінки. @jdavies: Додайте це до своєї відповіді. В іншому випадку він вийде з ладу при серіалізації.
SimonSimCity

1
Я виявив, що ви маєте справу з NULL випадками, інакше трапиться приємна помилка. Використання: ||| if (читач.TokenType == JsonToken.Null) повернути null; |||| джерело: stackoverflow.com/a/34185296/857291
Сезар

96

Вищеописане рішення для JsonCreationConverter<T> є у всьому Інтернеті, але має недолік, який проявляється в рідкісних випадках. Новий JsonReader, створений методом ReadJson, не успадковує жодне з оригінальних значень конфігурації читача (Культура, ДатаParseHandling, DateTimeZoneHandling, FloatParseHandling тощо). Ці значення слід скопіювати, перш ніж використовувати новий JsonReader в serializer.Populate ().

Це найкраще, що я міг би придумати, щоб виправити деякі проблеми з вищевказаною реалізацією, але я все ще думаю, що деякі речі нехтують:

Оновлення Я оновив це, щоб мати більш чіткий метод, який робить копію наявного зчитувача. Це просто інкапсулює процес копіювання через окремі настройки JsonReader. В ідеалі цю функцію можна було б підтримувати в самій бібліотеці Newtonsoft, але поки що ви можете використовувати наступне:

/// <summary>Creates a new reader for the specified jObject by copying the settings
/// from an existing reader.</summary>
/// <param name="reader">The reader whose settings should be copied.</param>
/// <param name="jToken">The jToken to create a new reader for.</param>
/// <returns>The new disposable reader.</returns>
public static JsonReader CopyReaderForObject(JsonReader reader, JToken jToken)
{
    JsonReader jTokenReader = jToken.CreateReader();
    jTokenReader.Culture = reader.Culture;
    jTokenReader.DateFormatString = reader.DateFormatString;
    jTokenReader.DateParseHandling = reader.DateParseHandling;
    jTokenReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
    jTokenReader.FloatParseHandling = reader.FloatParseHandling;
    jTokenReader.MaxDepth = reader.MaxDepth;
    jTokenReader.SupportMultipleContent = reader.SupportMultipleContent;
    return jTokenReader;
}

Це слід використовувати так:

public override object ReadJson(JsonReader reader,
                                Type objectType,
                                object existingValue,
                                JsonSerializer serializer)
{
    if (reader.TokenType == JsonToken.Null)
        return null;
    // Load JObject from stream
    JObject jObject = JObject.Load(reader);
    // Create target object based on JObject
    T target = Create(objectType, jObject);
    // Populate the object properties
    using (JsonReader jObjectReader = CopyReaderForObject(reader, jObject))
    {
        serializer.Populate(jObjectReader, target);
    }
    return target;
}

Старіші рішення наступні:

/// <summary>Base Generic JSON Converter that can help quickly define converters for specific types by automatically
/// generating the CanConvert, ReadJson, and WriteJson methods, requiring the implementer only to define a strongly typed Create method.</summary>
public abstract class JsonCreationConverter<T> : JsonConverter
{
    /// <summary>Create an instance of objectType, based properties in the JSON object</summary>
    /// <param name="objectType">type of object expected</param>
    /// <param name="jObject">contents of JSON object that will be deserialized</param>
    protected abstract T Create(Type objectType, JObject jObject);

    /// <summary>Determines if this converted is designed to deserialization to objects of the specified type.</summary>
    /// <param name="objectType">The target type for deserialization.</param>
    /// <returns>True if the type is supported.</returns>
    public override bool CanConvert(Type objectType)
    {
        // FrameWork 4.5
        // return typeof(T).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
        // Otherwise
        return typeof(T).IsAssignableFrom(objectType);
    }

    /// <summary>Parses the json to the specified type.</summary>
    /// <param name="reader">Newtonsoft.Json.JsonReader</param>
    /// <param name="objectType">Target type.</param>
    /// <param name="existingValue">Ignored</param>
    /// <param name="serializer">Newtonsoft.Json.JsonSerializer to use.</param>
    /// <returns>Deserialized Object</returns>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;

        // Load JObject from stream
        JObject jObject = JObject.Load(reader);

        // Create target object based on JObject
        T target = Create(objectType, jObject);

        //Create a new reader for this jObject, and set all properties to match the original reader.
        JsonReader jObjectReader = jObject.CreateReader();
        jObjectReader.Culture = reader.Culture;
        jObjectReader.DateParseHandling = reader.DateParseHandling;
        jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
        jObjectReader.FloatParseHandling = reader.FloatParseHandling;

        // Populate the object properties
        serializer.Populate(jObjectReader, target);

        return target;
    }

    /// <summary>Serializes to the specified type</summary>
    /// <param name="writer">Newtonsoft.Json.JsonWriter</param>
    /// <param name="value">Object to serialize.</param>
    /// <param name="serializer">Newtonsoft.Json.JsonSerializer to use.</param>
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}

7
Не забудьте подумати про CanWrite! (Я встановив це значення false) Ви можете закінчитись самореференційними циклами (я це зробив). stackoverflow.com/questions/12314438 / ...
Dribbel

1
Вам також не потрібно впроваджувати WriteJson? Як перетворювач знає, як перетворити з об’єкта в json?
Девід С.

15

Я просто подумав, що я поділюся також рішенням, заснованим на цьому, що працює з атрибутом Knowntype, використовуючи відображення, повинен був отримати похідний клас з будь-якого базового класу, рішення може отримати користь від рекурсії, щоб знайти найкращий клас, що відповідає, хоча мені це не потрібно в моєму випадок, узгодження проводиться за типом, який надається конвертору, якщо він має K knownTypes, він буде сканувати їх усіх, поки не збігається з типом, який має всі властивості всередині рядка json, буде обраний перший для відповідності.

використання просте, як:

 string json = "{ Name:\"Something\", LastName:\"Otherthing\" }";
 var ret  = JsonConvert.DeserializeObject<A>(json, new KnownTypeConverter());

у вищезгаданому випадку ret буде типу B.

Класи JSON:

[KnownType(typeof(B))]
public class A
{
   public string Name { get; set; }
}

public class B : A
{
   public string LastName { get; set; }
}

Код перетворювача:

/// <summary>
    /// Use KnownType Attribute to match a divierd class based on the class given to the serilaizer
    /// Selected class will be the first class to match all properties in the json object.
    /// </summary>
    public  class KnownTypeConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return System.Attribute.GetCustomAttributes(objectType).Any(v => v is KnownTypeAttribute);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            // Load JObject from stream
            JObject jObject = JObject.Load(reader);

            // Create target object based on JObject
            System.Attribute[] attrs = System.Attribute.GetCustomAttributes(objectType);  // Reflection. 

                // Displaying output. 
            foreach (System.Attribute attr in attrs)
            {
                if (attr is KnownTypeAttribute)
                {
                    KnownTypeAttribute k = (KnownTypeAttribute) attr;
                    var props = k.Type.GetProperties();
                    bool found = true;
                    foreach (var f in jObject)
                    {
                        if (!props.Any(z => z.Name == f.Key))
                        {
                            found = false;
                            break;
                        }
                    }

                    if (found)
                    {
                        var target = Activator.CreateInstance(k.Type);
                        serializer.Populate(jObject.CreateReader(),target);
                        return target;
                    }
                }
            }
            throw new ObjectNotFoundException();


            // Populate the object properties

        }

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

1
Мені дуже подобається це рішення, але я можу побачити проблему, коли існує кілька відомих типів, які мають однакові точні назви властивостей. Чи стикалися ви з цією проблемою? Дякую.
covo

8

Проект JsonSubTypes реалізує загальний перетворювач, який обробляє цю функцію за допомогою атрибутів.

Для конкретного зразка, представленого тут, як це працює:

    [JsonConverter(typeof(JsonSubtypes))]
    [JsonSubtypes.KnownSubTypeWithProperty(typeof(Employee), "JobTitle")]
    [JsonSubtypes.KnownSubTypeWithProperty(typeof(Artist), "Skill")]
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

    public class Employee : Person
    {
        public string Department { get; set; }
        public string JobTitle { get; set; }
    }

    public class Artist : Person
    {
        public string Skill { get; set; }
    }

    [TestMethod]
    public void Demo()
    {
        string json = "[{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," +
                      "{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," +
                      "{\"Skill\":\"Painter\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}]";


        var persons = JsonConvert.DeserializeObject<IReadOnlyCollection<Person>>(json);
        Assert.AreEqual("Painter", (persons.Last() as Artist)?.Skill);
    }

2
Дуже корисний перетворювач. Просто врятував мене години кодування перетворювача!
Карлос Родрігес

7

Це розширення до відповіді тотема. Це в основному те саме, але відповідність властивості базується на серіалізованому об'єкті json, а не відображає об’єкт .net. Це важливо, якщо ви використовуєте [JsonProperty], використовуєте CamelCasePropertyNamesContractResolver, або робите що-небудь інше, що призведе до того, що json не відповідає об’єкту .net.

Використання просте:

[KnownType(typeof(B))]
public class A
{
   public string Name { get; set; }
}

public class B : A
{
   public string LastName { get; set; }
}

Код перетворювача:

/// <summary>
/// Use KnownType Attribute to match a divierd class based on the class given to the serilaizer
/// Selected class will be the first class to match all properties in the json object.
/// </summary>
public class KnownTypeConverter : JsonConverter {
    public override bool CanConvert( Type objectType ) {
        return System.Attribute.GetCustomAttributes( objectType ).Any( v => v is KnownTypeAttribute );
    }

    public override bool CanWrite {
        get { return false; }
    }

    public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer ) {
        // Load JObject from stream
        JObject jObject = JObject.Load( reader );

        // Create target object based on JObject
        System.Attribute[ ] attrs = System.Attribute.GetCustomAttributes( objectType );  // Reflection. 

        // check known types for a match. 
        foreach( var attr in attrs.OfType<KnownTypeAttribute>( ) ) {
            object target = Activator.CreateInstance( attr.Type );

            JObject jTest;
            using( var writer = new StringWriter( ) ) {
                using( var jsonWriter = new JsonTextWriter( writer ) ) {
                    serializer.Serialize( jsonWriter, target );
                    string json = writer.ToString( );
                    jTest = JObject.Parse( json );
                }
            }

            var jO = this.GetKeys( jObject ).Select( k => k.Key ).ToList( );
            var jT = this.GetKeys( jTest ).Select( k => k.Key ).ToList( );

            if( jO.Count == jT.Count && jO.Intersect( jT ).Count( ) == jO.Count ) {
                serializer.Populate( jObject.CreateReader( ), target );
                return target;
            }
        }

        throw new SerializationException( string.Format( "Could not convert base class {0}", objectType ) );
    }

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

    private IEnumerable<KeyValuePair<string, JToken>> GetKeys( JObject obj ) {
        var list = new List<KeyValuePair<string, JToken>>( );
        foreach( var t in obj ) {
            list.Add( t );
        }
        return list;
    }
}

5

В якості іншого варіанту рішення відомого типу Totem ви можете використовувати відображення для створення загального розв'язувача типу, щоб уникнути необхідності використання відомих атрибутів типу.

Для цього використовується методика, аналогічна GenericResolver Juval Lowy для WCF.

Поки ваш базовий клас абстрактний або інтерфейс, відомі типи визначатимуться автоматично, а не повинні бути прикрашені відомими атрибутами типу.

У своєму власному випадку я вирішив використовувати властивість $ type для позначення типу в моєму об'єкті json, а не намагатися визначити його у властивостях, хоча ви можете запозичити тут інші рішення для використання визначення властивості.

 public class JsonKnownTypeConverter : JsonConverter
{
    public IEnumerable<Type> KnownTypes { get; set; }

    public JsonKnownTypeConverter() : this(ReflectTypes())
    {

    }
    public JsonKnownTypeConverter(IEnumerable<Type> knownTypes)
    {
        KnownTypes = knownTypes;
    }

    protected object Create(Type objectType, JObject jObject)
    {
        if (jObject["$type"] != null)
        {
            string typeName = jObject["$type"].ToString();
            return Activator.CreateInstance(KnownTypes.First(x => typeName == x.Name));
        }
        else
        {
            return Activator.CreateInstance(objectType);
        }
        throw new InvalidOperationException("No supported type");
    }

    public override bool CanConvert(Type objectType)
    {
        if (KnownTypes == null)
            return false;

        return (objectType.IsInterface || objectType.IsAbstract) && KnownTypes.Any(objectType.IsAssignableFrom);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // Load JObject from stream
        JObject jObject = JObject.Load(reader);

        // Create target object based on JObject
        var target = Create(objectType, jObject);
        // Populate the object properties
        serializer.Populate(jObject.CreateReader(), target);
        return target;
    }

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

    //Static helpers
    static Assembly CallingAssembly = Assembly.GetCallingAssembly();

    static Type[] ReflectTypes()
    {
        List<Type> types = new List<Type>();
        var referencedAssemblies = Assembly.GetExecutingAssembly().GetReferencedAssemblies();
        foreach (var assemblyName in referencedAssemblies)
        {
            Assembly assembly = Assembly.Load(assemblyName);
            Type[] typesInReferencedAssembly = GetTypes(assembly);
            types.AddRange(typesInReferencedAssembly);
        }

        return types.ToArray();
    }

    static Type[] GetTypes(Assembly assembly, bool publicOnly = true)
    {
        Type[] allTypes = assembly.GetTypes();

        List<Type> types = new List<Type>();

        foreach (Type type in allTypes)
        {
            if (type.IsEnum == false &&
               type.IsInterface == false &&
               type.IsGenericTypeDefinition == false)
            {
                if (publicOnly == true && type.IsPublic == false)
                {
                    if (type.IsNested == false)
                    {
                        continue;
                    }
                    if (type.IsNestedPrivate == true)
                    {
                        continue;
                    }
                }
                types.Add(type);
            }
        }
        return types.ToArray();
    }

Потім він може бути встановлений як форматер

GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new JsonKnownTypeConverter());

1

Ось ще одне рішення, яке дозволяє уникнути використання jObject.CreateReader(), а натомість створює нове JsonTextReader(яке поведінка використовується методом за замовчуванням JsonCreate.Deserialze:

public abstract class JsonCreationConverter<T> : JsonConverter
{
    protected abstract T Create(Type objectType, JObject jObject);

    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;

        // Load JObject from stream
        JObject jObject = JObject.Load(reader);

        // Create target object based on JObject
        T target = Create(objectType, jObject);

        // Populate the object properties
        StringWriter writer = new StringWriter();
        serializer.Serialize(writer, jObject);
        using (JsonTextReader newReader = new JsonTextReader(new StringReader(writer.ToString())))
        { 
            newReader.Culture = reader.Culture;
            newReader.DateParseHandling = reader.DateParseHandling;
            newReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
            newReader.FloatParseHandling = reader.FloatParseHandling;
            serializer.Populate(newReader, target);
        }

        return target;
    }

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

1

Багато разів реалізація буде існувати в тому ж просторі імен, що і інтерфейс. Отже, я придумав таке:

    public class InterfaceConverter : JsonConverter
    {
    public override bool CanWrite => false;
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var token = JToken.ReadFrom(reader);
        var typeVariable = this.GetTypeVariable(token);
        if (TypeExtensions.TryParse(typeVariable, out var implimentation))
        { }
        else if (!typeof(IEnumerable).IsAssignableFrom(objectType))
        {
            implimentation = this.GetImplimentedType(objectType);
        }
        else
        {
            var genericArgumentTypes = objectType.GetGenericArguments();
            var innerType = genericArgumentTypes.FirstOrDefault();
            if (innerType == null)
            {
                implimentation = typeof(IEnumerable);
            }
            else
            {
                Type genericType = null;
                if (token.HasAny())
                {
                    var firstItem = token[0];
                    var genericTypeVariable = this.GetTypeVariable(firstItem);
                    TypeExtensions.TryParse(genericTypeVariable, out genericType);
                }

                genericType = genericType ?? this.GetImplimentedType(innerType);
                implimentation = typeof(IEnumerable<>);
                implimentation = implimentation.MakeGenericType(genericType);
            }
        }

        return JsonConvert.DeserializeObject(token.ToString(), implimentation);
    }

    public override bool CanConvert(Type objectType)
    {
        return !typeof(IEnumerable).IsAssignableFrom(objectType) && objectType.IsInterface || typeof(IEnumerable).IsAssignableFrom(objectType) && objectType.GetGenericArguments().Any(t => t.IsInterface);
    }

    protected Type GetImplimentedType(Type interfaceType)
    {
        if (!interfaceType.IsInterface)
        {
            return interfaceType;
        }

        var implimentationQualifiedName = interfaceType.AssemblyQualifiedName?.Replace(interfaceType.Name, interfaceType.Name.Substring(1));
        return implimentationQualifiedName == null ? interfaceType : Type.GetType(implimentationQualifiedName) ?? interfaceType;
    }

    protected string GetTypeVariable(JToken token)
    {
        if (!token.HasAny())
        {
            return null;
        }

        return token.Type != JTokenType.Object ? null : token.Value<string>("$type");
    }
}

Тому ви можете включити це у всьому світі так:

public static JsonSerializerSettings StandardSerializerSettings => new JsonSerializerSettings
    {
        Converters = new List<JsonConverter>
        {
            new InterfaceConverter()
        }
    };

1

Використовуючи ідею тотема та злангнера , я створив таке, KnownTypeConverterщо зможе визначити найбільш відповідного спадкоємця, враховуючи, що дані json можуть не мати додаткових елементів.

Отже, служба надсилає відповідь JSON, яка містить масив документів (вхідних та вихідних). Документи мають як загальний набір елементів, так і різні. У цьому випадку елементи, пов'язані з вихідними документами, необов’язкові і можуть бути відсутніми.

У зв'язку з цим Documentбув створений базовий клас, який включає загальний набір властивостей. Також створено два класи спадкоємців: - OutgoingDocumentдодає два необов'язкові елементи "device_id"та "msg_id"; - IncomingDocumentдодає один обов’язковий елемент "sender_id";

Завдання полягало в тому, щоб створити перетворювач, який на основі даних json та інформації від KknownTypeAttribute зможе визначити найбільш відповідний клас, що дозволяє зберегти найбільшу кількість отриманої інформації. Слід також враховувати, що дані json можуть не мати додаткових елементів. Щоб зменшити кількість порівнянь елементів json та властивостей моделей даних, я вирішив не враховувати властивості базового класу, а співвіднести з елементами json лише властивості класів-спадкоємців.

Дані з послуги:

{
    "documents": [
        {
            "document_id": "76b7be75-f4dc-44cd-90d2-0d1959922852",
            "date": "2019-12-10 11:32:49",
            "processed_date": "2019-12-10 11:32:49",
            "sender_id": "9dedee17-e43a-47f1-910e-3a88ff6bc258",
        },
        {
            "document_id": "5044a9ac-0314-4e9a-9e0c-817531120753",
            "date": "2019-12-10 11:32:44",
            "processed_date": "2019-12-10 11:32:44",
        }
    ], 
    "total": 2
}

Моделі даних:

/// <summary>
/// Service response model
/// </summary>
public class DocumentsRequestIdResponse
{
    [JsonProperty("documents")]
    public Document[] Documents { get; set; }

    [JsonProperty("total")]
    public int Total { get; set; }
}

// <summary>
/// Base document
/// </summary>
[JsonConverter(typeof(KnownTypeConverter))]
[KnownType(typeof(OutgoingDocument))]
[KnownType(typeof(IncomingDocument))]
public class Document
{
    [JsonProperty("document_id")]
    public Guid DocumentId { get; set; }

    [JsonProperty("date")]
    public DateTime Date { get; set; }

    [JsonProperty("processed_date")]
    public DateTime ProcessedDate { get; set; } 
}

/// <summary>
/// Outgoing document
/// </summary>
public class OutgoingDocument : Document
{
    // this property is optional and may not be present in the service's json response
    [JsonProperty("device_id")]
    public string DeviceId { get; set; }

    // this property is optional and may not be present in the service's json response
    [JsonProperty("msg_id")]
    public string MsgId { get; set; }
}

/// <summary>
/// Incoming document
/// </summary>
public class IncomingDocument : Document
{
    // this property is mandatory and is always populated by the service
    [JsonProperty("sender_sys_id")]
    public Guid SenderSysId { get; set; }
}

Конвертор:

public class KnownTypeConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return System.Attribute.GetCustomAttributes(objectType).Any(v => v is KnownTypeAttribute);
    }

    public override bool CanWrite => false;

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // load the object 
        JObject jObject = JObject.Load(reader);

        // take custom attributes on the type
        Attribute[] attrs = Attribute.GetCustomAttributes(objectType);

        Type mostSuitableType = null;
        int countOfMaxMatchingProperties = -1;

        // take the names of elements from json data
        HashSet<string> jObjectKeys = GetKeys(jObject);

        // take the properties of the parent class (in our case, from the Document class, which is specified in DocumentsRequestIdResponse)
        HashSet<string> objectTypeProps = objectType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
            .Select(p => p.Name)
            .ToHashSet();

        // trying to find the right "KnownType"
        foreach (var attr in attrs.OfType<KnownTypeAttribute>())
        {
            Type knownType = attr.Type;
            if(!objectType.IsAssignableFrom(knownType))
                continue;

            // select properties of the inheritor, except properties from the parent class and properties with "ignore" attributes (in our case JsonIgnoreAttribute and XmlIgnoreAttribute)
            var notIgnoreProps = knownType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
                .Where(p => !objectTypeProps.Contains(p.Name)
                            && p.CustomAttributes.All(a => a.AttributeType != typeof(JsonIgnoreAttribute) && a.AttributeType != typeof(System.Xml.Serialization.XmlIgnoreAttribute)));

            //  get serializable property names
            var jsonNameFields = notIgnoreProps.Select(prop =>
            {
                string jsonFieldName = null;
                CustomAttributeData jsonPropertyAttribute = prop.CustomAttributes.FirstOrDefault(a => a.AttributeType == typeof(JsonPropertyAttribute));
                if (jsonPropertyAttribute != null)
                {
                    // take the name of the json element from the attribute constructor
                    CustomAttributeTypedArgument argument = jsonPropertyAttribute.ConstructorArguments.FirstOrDefault();
                    if(argument != null && argument.ArgumentType == typeof(string) && !string.IsNullOrEmpty((string)argument.Value))
                        jsonFieldName = (string)argument.Value;
                }
                // otherwise, take the name of the property
                if (string.IsNullOrEmpty(jsonFieldName))
                {
                    jsonFieldName = prop.Name;
                }

                return jsonFieldName;
            });


            HashSet<string> jKnownTypeKeys = new HashSet<string>(jsonNameFields);

            // by intersecting the sets of names we determine the most suitable inheritor
            int count = jObjectKeys.Intersect(jKnownTypeKeys).Count();

            if (count == jKnownTypeKeys.Count)
            {
                mostSuitableType = knownType;
                break;
            }

            if (count > countOfMaxMatchingProperties)
            {
                countOfMaxMatchingProperties = count;
                mostSuitableType = knownType;
            }
        }

        if (mostSuitableType != null)
        {
            object target = Activator.CreateInstance(mostSuitableType);
            using (JsonReader jObjectReader = CopyReaderForObject(reader, jObject))
            {
                serializer.Populate(jObjectReader, target);
            }
            return target;
        }

        throw new SerializationException($"Could not serialize to KnownTypes and assign to base class {objectType} reference");
    }

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

    private HashSet<string> GetKeys(JObject obj)
    {
        return new HashSet<string>(((IEnumerable<KeyValuePair<string, JToken>>) obj).Select(k => k.Key));
    }

    public static JsonReader CopyReaderForObject(JsonReader reader, JObject jObject)
    {
        JsonReader jObjectReader = jObject.CreateReader();
        jObjectReader.Culture = reader.Culture;
        jObjectReader.DateFormatString = reader.DateFormatString;
        jObjectReader.DateParseHandling = reader.DateParseHandling;
        jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
        jObjectReader.FloatParseHandling = reader.FloatParseHandling;
        jObjectReader.MaxDepth = reader.MaxDepth;
        jObjectReader.SupportMultipleContent = reader.SupportMultipleContent;
        return jObjectReader;
    }
}

PS: У моєму випадку, якщо жодного спадкоємця не було обрано перетворювачем (це може статися, якщо дані JSON містять інформацію лише з базового класу або дані JSON не містять додаткових елементів з OutgoingDocument), то об'єкт OutgoingDocumentкласу буде створено, оскільки воно зазначається першим у списку KnownTypeAttributeатрибутів. За вашим бажанням ви можете варіювати реалізацію KnownTypeConverterцієї ситуації.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.