110 votes

Désérialiser des classes json polymorphes sans information de type en utilisant json.net

Cet appel Imgur api renvoie une liste contenant à la fois les classes Gallery Image et Gallery Album représentées en JSON.

Je ne vois pas comment les désérialiser automatiquement en utilisant Json.NET étant donné qu'il n'y a pas de propriété $type indiquant au désérialiseur quelle classe doit être représentée. Il y a une propriété appelée "IsAlbum" qui peut être utilisée pour différencier les deux.

Cette question semble montrer une méthode mais cela semble être un peu bidouillage.

Comment puis-je désérialiser ces classes? (en utilisant C#, Json.NET).

Données d'exemple:

Gallery Image

{
    "id": "OUHDm",
    "title": "Mon dessin le plus récent. 100 heures passées dessus.",
        ...
    "is_album": false
}

Gallery Album

{
    "id": "lDRB2",
    "title": "Bureau Imgur",
    ...
    "is_album": true,
    "nombre_images": 3,
    "images": [
        {
            "id": "24nLu",
            ...
            "link": "http://i.imgur.com/24nLu.jpg"
        },
        {
            "id": "Ziz25",
            ...
            "link": "http://i.imgur.com/Ziz25.jpg"
        },
        {
            "id": "9tzW6",
            ...
            "link": "http://i.imgur.com/9tzW6.jpg"
        }
    ]
}
}

0voto

Dai Points 24530

La réponse de @ИгорьОрлов fonctionne lorsque vous avez des types qui ne peuvent être instanciés directement que par JSON.net (en raison de [JsonConstructor] et/ou de l'utilisation de [JsonProperty] directement sur les paramètres du constructeur. Cependant, écraser contract.Converter = null ne fonctionne pas lorsque JSON.net a déjà mis en cache le convertisseur à utiliser.

(Cela ne poserait pas de problème si JSON.NET utilisait des types immuables pour indiquer quand les données et la configuration ne sont plus modifiables, <em>le soupir</em>)

Dans mon cas, j'ai fait ceci:

  1. Implémenter un JsonConverter personnalisé (où T est la classe de base de mon DTO).
  2. Définir une sous-classe de DefaultContractResolver qui remplace ResolveContractConverter pour renvoyer mon JsonConverter personnalisé pour seulement la classe de base.

En détail, et par exemple:

Supposons que j'ai ces DTOs immuables qui représentent un système de fichiers distant (donc il y a DirectoryDto et FileDto qui héritent tous deux de FileSystemDto, tout comme DirectoryInfo et FileInfo dérivent de System.IO.FileSystemInfo):

public enum DtoKind
{
    None = 0,
    File,
    Directory
}

public abstract class FileSystemDto
{
    protected FileSystemDto( String name, DtoKind kind )
    {
        this.Name = name ?? throw new ArgumentNullException(nameof(name));
        this.Kind = kind;
    }

    [JsonProperty( "name" )]
    public String Name { get; }

    [JsonProperty( "kind" )]
    public String Kind { get; }
}

public class FileDto : FileSystemDto
{
    [JsonConstructor]
    public FileDto(
        [JsonProperty("name"  )] String  name,
        [JsonProperty("length")] Int64   length,
        [JsonProperty("kind")  ] DtoKind kind
    )
        : base( name: name, kind: kind )
    {
        if( kind != DtoKind.File ) throw new InvalidOperationException( "blargh" );
        this.Length = length;
    }

    [JsonProperty( "length" )]
    public Int64 Length { get; }
}

public class DirectoryDto : FileSystemDto
{
    [JsonConstructor]
    public FileDto(
        [JsonProperty("name")] String  name,
        [JsonProperty("kind")] DtoKind kind
    )
        : base( name: name, kind: kind )
    {
        if( kind != DtoKind.Directory ) throw new InvalidOperationException( "blargh" );
    }
}

Supposons que j'ai un tableau JSON de FileSystemDto:

[
    { "name": "foo.txt", "kind": "File", "length": 12345 },
    { "name": "bar.txt", "kind": "File", "length": 12345 },
    { "name": "subdir", "kind": "Directory" },
]

Je veux que Json.net désérialise cela en List...

Donc, définissez une sous-classe de DefaultContractResolver (ou si vous avez déjà une implémentation de résolveur, sous-classez (ou composez) cela) et remplacez ResolveContractConverter:

public class MyContractResolver : DefaultContractResolver
{
    protected override JsonConverter? ResolveContractConverter( Type objectType )
    {
        if( objectType == typeof(FileSystemDto) )
        {
            return MyJsonConverter.Instance;
        }
        else if( objectType == typeof(FileDto ) )
        {
            // utiliser par défaut
        }
        else if( objectType == typeof(DirectoryDto) )
        {
            // utiliser par défaut
        }

        return base.ResolveContractConverter( objectType );
    }
}

Ensuite, implémentez MyJsonConverter:

public class MyJsonConverter : JsonConverter
{
    public static MyJsonConverter Instance { get; } = new MyJsonConverter();

    private MyJsonConverter() {}

    // TODO: Remplacer `CanWrite => false` et `WriteJson { throw; }` si vous le souhaitez.

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

        if( objectType == typeof(FileSystemDto) )
        {
            JObject jsonObject = JObject.Load( reader );
            if( jsonObject.Property( "kind" )?.Value is JValue jv && jv.Value is String kind )
            {
                if( kind == "File" )
                {
                    return jsonObject.ToObject( serializer );
                }
                else if( kind == "Directory" )
                {
                    return jsonObject.ToObject( serializer );
                }
            }
        }

        return null; // ou lancer, selon votre strictesse.
    }
}

Ensuite, pour désérialiser, utilisez une instance de JsonSerializer avec le ContractResolver correctement défini, par exemple:

public static IReadOnlyList DeserializeFileSystemJsonArray( String json )
{
    JsonSerializer jss = new JsonSerializer()
    {
        ContractResolver = new KuduDtoContractResolver()
    };

    using( StringReader strRdr = new StringReader( json ) )
    using( JsonTextReader jsonRdr = new JsonTextReader( strRdr ) )
    {
        List? list = jss.Deserialize< List >( jsonRdr );
        // TODO: Lancer si `list` est null.
        return list;
    }
}

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