How to implement custom JsonConverter in JSON.NET to deserialize a List of base class objects?

前端 未结 9 1799
星月不相逢
星月不相逢 2020-11-21 05:07

I am trying to extend the JSON.net example given here http://james.newtonking.com/projects/json/help/CustomCreationConverter.html

I have another sub class deriving

相关标签:
9条回答
  • 2020-11-21 05:44

    This is an expansion to totem's answer. It does basically the same thing but the property matching is based on the serialized json object, not reflect the .net object. This is important if you're using [JsonProperty], using the CamelCasePropertyNamesContractResolver, or doing anything else that will cause the json to not match the .net object.

    Usage is simple:

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

    Converter code:

    /// <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;
        }
    }
    
    0 讨论(0)
  • 2020-11-21 05:47

    Here's another solution that avoids the use of jObject.CreateReader(), and instead creates a new JsonTextReader (which is the behavior used by the default JsonCreate.Deserialze method:

    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);
        }
    }
    
    0 讨论(0)
  • 2020-11-21 05:52

    As another variation on Totem's known type solution, you can use reflection to create a generic type resolver to avoid the need to use known type attributes.

    This uses a technique similar to Juval Lowy's GenericResolver for WCF.

    As long as your base class is abstract or an interface, the known types will be automatically determined rather than having to be decorated with known type attributes.

    In my own case I opted to use a $type property to designate type in my json object rather than try to determine it from the properties, though you could borrow from other solutions here to use property based determination.

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

    It can then be installed as a formatter

    GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new JsonKnownTypeConverter());
    
    0 讨论(0)
  • 2020-11-21 05:53

    Using the standard CustomCreationConverter, I was struggling to work how to generate the correct type (Person or Employee), because in order to determine this you need to analyse the JSON and there is no built in way to do this using the Create method.

    I found a discussion thread pertaining to type conversion and it turned out to provide the answer. Here is a link: Type converting.

    What's required is to subclass JsonConverter, overriding the ReadJson method and creating a new abstract Create method which accepts a JObject.

    The JObject class provides a means to load a JSON object and provides access to the data within this object.

    The overridden ReadJson method creates a JObject and invokes the Create method (implemented by our derived converter class), passing in the JObject instance.

    This JObject instance can then be analysed to determine the correct type by checking existence of certain fields.

    Example

    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;
        }
    }
    
    0 讨论(0)
  • 2020-11-21 05:55

    The above solution for the JsonCreationConverter<T> is all over the internet, but has a flaw that manifests itself in rare occasions. The new JsonReader created in the ReadJson method does not inherit any of the original reader's configuration values (Culture, DateParseHandling, DateTimeZoneHandling, FloatParseHandling, etc...). These values should be copied over before using the new JsonReader in serializer.Populate().

    This is the best I could come up with to fix some of the problems with the above implementation, but I still think there are some things being overlooked:

    Update I updated this to have a more explicit method that makes a copy of an existing reader. This just encapsulates the process of copying over individual JsonReader settings. Ideally this function would be maintained in the Newtonsoft library itself, but for now, you can use the following:

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

    This should be used as follows:

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

    Older solution follows:

    /// <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);
        }
    }
    
    0 讨论(0)
  • 2020-11-21 05:58

    Using the idea of totem and zlangner, I have created a KnownTypeConverter that will be able to determine the most appropriate inheritor, while taking into account that json data may not have optional elements.

    So, the service sends a JSON response that contains an array of documents (incoming and outgoing). Documents have both a common set of elements and different ones. In this case, the elements related to the outgoing documents are optional and may be absent.

    In this regard, a base class Document was created that includes a common set of properties. Two inheritor classes are also created: - OutgoingDocument adds two optional elements "device_id" and "msg_id"; - IncomingDocument adds one mandatory element "sender_id";

    The task was to create a converter that based on json data and information from KnownTypeAttribute will be able to determine the most appropriate class that allows you to save the largest amount of information received. It should also be taken into account that json data may not have optional elements. To reduce the number of comparisons of json elements and properties of data models, I decided not to take into account the properties of the base class and to correlate with json elements only the properties of the inheritor classes.

    Data from the service:

    {
        "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
    }
    

    Data models:

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

    Converter:

    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: In my case, if no one inheritor has not been selected by converter (this can happen if the JSON data contains information only from the base class or the JSON data does not contain optional elements from the OutgoingDocument), then an object of the OutgoingDocument class will be created, since it is listed first in the list of KnownTypeAttribute attributes. At your request, you can vary the implementation of the KnownTypeConverter in this situation.

    0 讨论(0)
提交回复
热议问题