320 votes

Comment implémenter un JsonConverter personnalisé dans JSON.NET pour désérialiser une liste d'objets de classe de base ?

J'essaie d'étendre l'exemple de JSON.net donné ici. http://james.newtonking.com/projects/json/help/CustomCreationConverter.html

J'ai une autre sous-classe qui dérive de la classe de base/Interface.

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

Comment désérialiser le Json suivant en List< Person > ?

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

Je ne veux pas utiliser TypeNameHandling JsonSerializerSettings. Je cherche spécifiquement une implémentation personnalisée de JsonConverter pour gérer cela. La documentation et les exemples à ce sujet sont plutôt rares sur le net. Je ne parviens pas à mettre en œuvre correctement la méthode ReadJson() surchargée dans JsonConverter.

0 votes

330voto

jdavies Points 7712

En utilisant la norme CustomCreationConverter j'ai eu du mal à trouver comment générer le bon type ( Person ou Employee ), car pour le déterminer, il faut analyser le JSON et il n'y a pas de moyen intégré de le faire en utilisant la commande Create méthode.

J'ai trouvé un fil de discussion concernant la conversion de type et il s'est avéré fournir la réponse. Voici le lien : Conversion de type .

Ce qu'il faut, c'est sous-classer JsonConverter en remplaçant le ReadJson et la création d'une nouvelle abstraction Create qui accepte un JObject .

La classe JObject permet de charger un objet JSON et d'accéder aux données qu'il contient. permet d'accéder aux données contenues dans cet objet.

La surcharge ReadJson crée un JObject et invoque le Create (mise en œuvre par notre classe de convertisseur dérivée), en passant dans la méthode JObject instance.

Ce site JObject L'instance peut ensuite être analysée pour déterminer le type correct en vérifiant l'existence de certains champs.

Exemple

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

8 votes

Il serait bien d'avoir la méthode WriteJson implémentée aussi, et de fournir une méthode abstraite pour stringifier le type.

66 votes

NOTE : Cette solution est partout sur Internet, mais elle présente un défaut qui se manifeste en de rares occasions. La nouvelle JsonReader créé dans le ReadJson n'hérite d'aucune des valeurs de configuration du lecteur d'origine ( Culture , DateParseHandling , DateTimeZoneHandling , FloatParseHandling etc...). Ces valeurs doivent être copiées avant d'utiliser la nouvelle fonction JsonReader sur serializer.Populate() .

9 votes

Pour éviter de créer un nouveau JsonReader (pour les raisons mentionnées par @Alain), ou si vous avez besoin de décider du type d'objet créé en fonction de certaines valeurs du parent, voyez cette solution stackoverflow.com/a/22539730/1038496 . Cela me semble plus efficace et plus clair (même pour ce type de problème).

105voto

Alain Points 10079

La solution ci-dessus pour le JsonCreationConverter<T> est partout sur internet, mais a un défaut qui se manifeste en de rares occasions. Le nouveau JsonReader créé par la méthode ReadJson n'hérite d'aucune des valeurs de configuration du lecteur original (Culture, DateParseHandling, DateTimeZoneHandling, FloatParseHandling, etc...). Ces valeurs doivent être copiées avant d'utiliser le nouveau JsonReader dans serializer.Populate().

C'est la meilleure solution que j'ai pu trouver pour résoudre certains des problèmes de l'implémentation ci-dessus, mais je pense toujours que certaines choses ont été négligées :

Mise à jour J'ai mis à jour ceci pour avoir une méthode plus explicite qui fait une copie d'un lecteur existant. Ceci ne fait qu'encapsuler le processus de copie des paramètres individuels de JsonReader. Idéalement, cette fonction devrait être maintenue dans la bibliothèque Newtonsoft elle-même, mais pour l'instant, vous pouvez utiliser ce qui suit :

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

Elle doit être utilisée comme suit :

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

Une solution plus ancienne suit :

/// <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 votes

N'oubliez pas de penser à CanWrite ! (je l'ai mis à false) Vous pouvez finir avec des boucles auto-référencées (je l'ai fait). stackoverflow.com/questions/12314438/

1 votes

Ne devez-vous pas également implémenter WriteJson ? Comment le convertisseur sait-il comment convertir l'objet en json ?

17voto

totem Points 223

J'ai juste pensé que je partagerais une solution également basée sur ceci qui fonctionne avec l'attribut Knowntype en utilisant la réflexion, j'ai dû obtenir une classe dérivée de n'importe quelle classe de base, la solution peut bénéficier de la récursion pour trouver la meilleure classe correspondante bien que je n'en ai pas eu besoin dans mon cas, la correspondance est faite par le type donné au convertisseur s'il a KnownTypes il les scannera tous jusqu'à ce qu'il trouve un type qui a toutes les propriétés dans la chaîne json, le premier à correspondre sera choisi.

L'utilisation est aussi simple que :

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

dans le cas ci-dessus ret sera de type B.

Classes JSON :

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

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

Code convertisseur :

/// <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 votes

J'aime beaucoup cette solution, mais je vois un problème lorsqu'il y a plusieurs types connus qui ont exactement les mêmes noms de propriétés. Avez-vous rencontré ce problème ? Merci.

7voto

zlangner Points 1

C'est une extension de la réponse de Totem. Elle fait essentiellement la même chose mais la correspondance des propriétés est basée sur l'objet json sérialisé, et non sur l'objet .net. Ceci est important si vous utilisez [JsonProperty], si vous utilisez le CamelCasePropertyNamesContractResolver, ou si vous faites quoi que ce soit d'autre qui fasse que le json ne corresponde pas à l'objet .net.

L'utilisation est simple :

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

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

Code convertisseur :

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

1voto

Alain Points 10079

Voici une autre solution qui évite l'utilisation de jObject.CreateReader() et crée à la place une nouvelle JsonTextReader (qui est le comportement utilisé par l'option par défaut JsonCreate.Deserialze méthode :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X