使用Json.net反序列化多态的Json类别,但不包含类型信息

115

这个Imgur api调用返回一个包含JSON中表示的画廊图片画廊相册类的列表。

我不知道如何使用Json.NET自动反序列化它们,因为没有 $type 属性告诉反序列化器要表示哪个类。但是有一个名为“IsAlbum”的属性可以用来区分两者。

这个问题似乎展示了一种方法,但看起来有点像hack。

我该如何反序列化这些类?(使用C#,Json.NET).

示例数据:

Gallery Image

{
    "id": "OUHDm",
    "title": "My most recent drawing. Spent over 100 hours.",
        ...
    "is_album": false
}

图库相册

{
    "id": "lDRB2",
    "title": "Imgur Office",
    ...
    "is_album": true,
    "images_count": 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"
        }
    ]
}
}

你想将Json字符串放入类中吗?而且我不明白你所说的“没有$type属性”的意思。 - gunr2171
2
是的,我有JSON字符串并希望将其反序列化为C#类。Json.NET似乎使用一个名为"$type"的属性来区分数组中保存的不同类型。但这个数据没有那个属性,只使用了“IsAlbum”属性。 - Peter Kneale
如果您正在使用 System.Text.Json,请参考 is-polymorphic-deserialization-possible-in-system-text-json - undefined
6个回答

150

通过创建一个自定义的JsonConverter来处理对象实例化,你可以相对容易地完成这个操作。假设你的类定义如下:

public abstract class GalleryItem
{
    public string id { get; set; }
    public string title { get; set; }
    public string link { get; set; }
    public bool is_album { get; set; }
}

public class GalleryImage : GalleryItem
{
    // ...
}

public class GalleryAlbum : GalleryItem
{
    public int images_count { get; set; }
    public List<GalleryImage> images { get; set; }
}

您可以按照以下方式创建转换器:

public class GalleryItemConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(GalleryItem).IsAssignableFrom(objectType);
    }

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

        // Using a nullable bool here in case "is_album" is not present on an item
        bool? isAlbum = (bool?)jo["is_album"];

        GalleryItem item;
        if (isAlbum.GetValueOrDefault())
        {
            item = new GalleryAlbum();
        }
        else
        {
            item = new GalleryImage();
        }

        serializer.Populate(jo.CreateReader(), item);

        return item;
    }

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

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

这是一个展示转换器功能的示例程序:

class Program
{
    static void Main(string[] args)
    {
        string json = @"
        [
            {
                ""id"": ""OUHDm"",
                ""title"": ""My most recent drawing. Spent over 100 hours."",
                ""link"": ""http://i.imgur.com/OUHDm.jpg"",
                ""is_album"": false
            },
            {
                ""id"": ""lDRB2"",
                ""title"": ""Imgur Office"",
                ""link"": ""http://alanbox.imgur.com/a/lDRB2"",
                ""is_album"": true,
                ""images_count"": 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""
                    }
                ]
            }
        ]";

        List<GalleryItem> items = 
            JsonConvert.DeserializeObject<List<GalleryItem>>(json, 
                new GalleryItemConverter());

        foreach (GalleryItem item in items)
        {
            Console.WriteLine("id: " + item.id);
            Console.WriteLine("title: " + item.title);
            Console.WriteLine("link: " + item.link);
            if (item.is_album)
            {
                GalleryAlbum album = (GalleryAlbum)item;
                Console.WriteLine("album images (" + album.images_count + "):");
                foreach (GalleryImage image in album.images)
                {
                    Console.WriteLine("    id: " + image.id);
                    Console.WriteLine("    link: " + image.link);
                }
            }
            Console.WriteLine();
        }
    }
}

以下是上述程序的输出:

id: OUHDm
title: My most recent drawing. Spent over 100 hours.
link: http://i.imgur.com/OUHDm.jpg

id: lDRB2
title: Imgur Office
link: http://alanbox.imgur.com/a/lDRB2
album images (3):
    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

Fiddle: https://dotnetfiddle.net/1kplME


25
如果多态对象是递归的,即一个专辑可能包含其他专辑,则此方法无法正常工作。在转换器中,应该使用Serializer.Populate()而不是item.ToObject()。请参阅https://dev59.com/bF4b5IYBdhLWcg3wchRL。 - Ivan Krivyakov
9
如果有人尝试这种方法,结果会导致无限循环(最终导致堆栈溢出),那么您可能想要使用“ Populate”方法而不是“ToObject”。请参阅以下答案:https://dev59.com/FYLba4cB1Zd3GeqPhad2和https://dev59.com/bF4b5IYBdhLWcg3wchRL。我在这里的 gist 中提供了这两种方法的示例:https://gist.github.com/chrisoldwood/b604d69543a5fe5896a94409058c7a95。 - Chris Oldwood
我在各种关于CustomCreationConverter的答案中迷失了8个小时,最终找到了这个可行的答案,感觉茅塞顿开。我的内部对象包含其类型作为字符串,并且我使用它进行转换,就像这样。**JObject item = JObject.Load(reader);Type type = Type.GetType(item["Type"].Value<string>());return item.ToObject(type);** - Furkan Ekinci
1
只要你不在基类上放置转换器属性,这对我来说是有效的。转换器必须通过序列化程序(通过设置等)注入,并且在CanConvert中仅检查基本类型。我正在考虑使用Populate(),但我真的不喜欢这两种方法。 - xtravar
2
我已经修复了转换器,使用JsonSerializer.Populate()而不是Ivan和Chris建议的JObject.ToObject()。这将避免递归循环问题,并使转换器能够成功地与属性一起使用。 - Brian Rogers
不幸的是,这种方法不能与[JsonConstructor]一起使用 - 哎呀 - Dai

73

使用JsonSubTypes属性与Json.NET轻松实现。

    [JsonConverter(typeof(JsonSubtypes), "is_album")]
    [JsonSubtypes.KnownSubType(typeof(GalleryAlbum), true)]
    [JsonSubtypes.KnownSubType(typeof(GalleryImage), false)]
    public abstract class GalleryItem
    {
        public string id { get; set; }
        public string title { get; set; }
        public string link { get; set; }
        public bool is_album { get; set; }
    }

    public class GalleryImage : GalleryItem
    {
        // ...
    }

    public class GalleryAlbum : GalleryItem
    {
        public int images_count { get; set; }
        public List<GalleryImage> images { get; set; }
    }

12
这应该是最佳答案。我花了一整天的时间研究了数十个作者编写的自定义JsonConverter类来解决这个问题。但是你的NuGet包只需要三行代码就替代了我所有的努力。干得好先生,真棒! - Kenneth Cochran
1
在Java世界中,Jackson库通过@JsonSubTypes属性提供类似的支持。请参见https://dev59.com/1VcO5IYBdhLWcg3wrDRq#45447923以获取另一个用例示例(还有@KonstantinPelepelin在评论中提供的Cage / Animal示例)。 - vulcan raven
1
这确实是最简单的答案,但不幸的是它会带来性能上的代价。我发现使用手写转换器(如@BrianRogers的答案所示)进行反序列化可以提高2-3倍的速度。 - Sven Vranckx
1
@Frank 状态应该被修复,这是由于该依赖项在测试中被使用,而工具未能找到许可证:https://www.nuget.org/packages/TaskParallelLibrary/。而JsonSubTypes nuget工件只有一个外部依赖项Newtonsoft.Json。 - manuc66
1
在.NET 7中,他们开箱即用地添加了类似的功能:https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-7-0 - C-F
显示剩余4条评论

4

升级到 Brian Rogers 的回答。关于"使用Serializer.Populate()替代item.ToObject()"。 如果派生类型具有构造函数或者其中一些具有自定义转换器,您必须使用通用的方式来反序列化JSON。 因此,您必须让NewtonJson实例化新对象的工作。通过这种方式,您可以在自定义的JsonConverter中实现它:

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    ..... YOU Code For Determine Real Type of Json Record .......

    // 1. Correct ContractResolver for you derived type
    var contract = serializer.ContractResolver.ResolveContract(DeterminedType);
    if (converter != null && !typeDeserializer.Type.IsAbstract && converter.GetType() == GetType())
    {
        contract.Converter = null; // Clean Wrong Converter grabbed by DefaultContractResolver from you base class for derived class
    }

    // Deserialize in general way           
    var jTokenReader = new JTokenReader(jObject);
    var result = serializer.Deserialize(jTokenReader, DeterminedType);

    return (result);
}

如果您需要递归对象,这将起作用。


这段代码在多线程环境下不安全,因为默认的契约解析器会缓存契约。在契约上设置选项(如其转换器)将会导致它在并发甚至后续调用中行为不同。 - Dan Davies Brackett
serializer.ContractResolver.ResolveContract(DeterminedType) - 返回已缓存的合同。因此,contract.Converter = null; 更改缓存对象。它只会更改缓存对象上的引用,并且是线程安全的。 - Игорь Орлов
这实际上是一个非常好的答案,它保持了一切非常简单。如果您使用Serializer.Populate(),则需要自己创建对象,并且所有创建对象的逻辑(例如JsonConstructor属性)都将被忽略。 - Erik A. Brandstadmoen
当我的基类型BaseDto[JsonConverter(typeof(MyConverter))]时,这对我起作用,但是当我删除属性(但仍在JsonSerializer.Converters中指定MyConverter)时,它停止工作,因为contract.Converter始终为null。 我不确定如何向JsonSerializer指示Dto的子类也应使用相同的转换器,嗯。(不幸的是,JsonConverter<T>使CanConvert方法sealed,呃!) - Dai

1
以下实现应该允许您在不更改类设计方式的情况下进行反序列化,并使用除 $type 之外的字段来决定要将其反序列化为什么。
public class GalleryImageConverter : JsonConverter
{   
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(GalleryImage) || objectType == typeof(GalleryAlbum));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        try
        {
            if (!CanConvert(objectType))
                throw new InvalidDataException("Invalid type of object");
            JObject jo = JObject.Load(reader);
            // following is to avoid use of magic strings
            var isAlbumPropertyName = ((MemberExpression)((Expression<Func<GalleryImage, bool>>)(s => s.is_album)).Body).Member.Name;
            JToken jt;
            if (!jo.TryGetValue(isAlbumPropertyName, StringComparison.InvariantCultureIgnoreCase, out jt))
            {
                return jo.ToObject<GalleryImage>();
            }
            var propValue = jt.Value<bool>();
            if(propValue) {
                resultType = typeof(GalleryAlbum);
            }
            else{
                resultType = typeof(GalleryImage);
            }
            var resultObject = Convert.ChangeType(Activator.CreateInstance(resultType), resultType);
            var objectProperties=resultType.GetProperties();
            foreach (var objectProperty in objectProperties)
            {
                var propType = objectProperty.PropertyType;
                var propName = objectProperty.Name;
                var token = jo.GetValue(propName, StringComparison.InvariantCultureIgnoreCase);
                if (token != null)
                {
                    objectProperty.SetValue(resultObject,token.ToObject(propType)?? objectProperty.GetValue(resultObject));
                }
            }
            return resultObject;
        }
        catch (Exception ex)
        {
            throw;
        }
    }

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

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

1

我只是发布这篇文章来澄清一些混淆的问题。如果你正在使用预定义格式并需要反序列化它,以下是我发现最好的方法,并演示了机制,以便其他人可以根据需要进行调整。

public class BaseClassConverter : JsonConverter
    {
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var j = JObject.Load(reader);
            var retval = BaseClass.From(j, serializer);
            return retval;
        }

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

        public override bool CanConvert(Type objectType)
        {
            // important - do not cause subclasses to go through this converter
            return objectType == typeof(BaseClass);
        }
    }

    // important to not use attribute otherwise you'll infinite loop
    public abstract class BaseClass
    {
        internal static Type[] Types = new Type[] {
            typeof(Subclass1),
            typeof(Subclass2),
            typeof(Subclass3)
        };

        internal static Dictionary<string, Type> TypesByName = Types.ToDictionary(t => t.Name.Split('.').Last());

        // type property based off of class name
        [JsonProperty(PropertyName = "type", Required = Required.Always)]
        public string JsonObjectType { get { return this.GetType().Name.Split('.').Last(); } set { } }

        // convenience method to deserialize a JObject
        public static new BaseClass From(JObject obj, JsonSerializer serializer)
        {
            // this is our object type property
            var str = (string)obj["type"];

            // we map using a dictionary, but you can do whatever you want
            var type = TypesByName[str];

            // important to pass serializer (and its settings) along
            return obj.ToObject(type, serializer) as BaseClass;
        }


        // convenience method for deserialization
        public static BaseClass Deserialize(JsonReader reader)
        {
            JsonSerializer ser = new JsonSerializer();
            // important to add converter here
            ser.Converters.Add(new BaseClassConverter());

            return ser.Deserialize<BaseClass>(reader);
        }
    }

如果不使用已注释为“重要”的[JsonConverter()]属性,如何在使用隐式转换时使用它?例如:通过[FromBody]属性进行反序列化? - Alex McMillan
1
我认为您可以简单地编辑全局JsonFormatter的设置以包含此转换器。请参见https://stackoverflow.com/questions/41629523/using-a-custom-json-formatter-for-web-api-2 - xtravar

0

@ИгорьОрлов的答案适用于仅能由JSON.net直接实例化的类型(因为[JsonConstructor] 和/或在构造函数参数上直接使用[JsonProperty]),但是当JSON.net已经缓存了要使用的转换器时,覆盖contract.Converter = null 不起作用

(如果JSON.NET使用不可变类型来指示数据和配置何时不再可变,那就不会有这个问题,叹气

在我的情况下,我做了以下操作:

  1. 实现自定义的JsonConverter<T>(其中T是我的DTO的基类)。
  2. 定义一个DefaultContractResolver子类,覆盖ResolveContractConverter以仅为基类返回我的自定义JsonConverter

详细说明,以及示例:

假设我有这些不可变的DTO,它们代表远程文件系统(因此有DirectoryDtoFileDto,它们都继承自FileSystemDto,就像DirectoryInfoFileInfoSystem.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" );
    }
}

假设我有一个JSON数组FileSystemDto:
[
    { "name": "foo.txt", "kind": "File", "length": 12345 },
    { "name": "bar.txt", "kind": "File", "length": 12345 },
    { "name": "subdir", "kind": "Directory" },
]

我想要使用Json.net将其反序列化为List<FileSystemDto>...

因此,定义一个DefaultContractResolver的子类(或者如果您已经有一个解析器实现,则子类化(或组合)该解析器),并覆盖ResolveContractConverter

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

        return base.ResolveContractConverter( objectType );
    }
}

然后实现MyJsonConverter

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

    private MyJsonConverter() {}

    // TODO: Override `CanWrite => false` and `WriteJson { throw; }` if you like.

    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<FileDto>( serializer );
                }
                else if( kind == "Directory" )
                {
                    return jsonObject.ToObject<DirectoryDto>( serializer );
                }
            }
        }

        return null; // or throw, depending on your strictness.
    }
}

然后,要进行反序列化,请使用正确设置了ContractResolverJsonSerializer实例,例如:

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

    using( StringReader strRdr = new StringReader( json ) )
    using( JsonTextReader jsonRdr = new JsonTextReader( strRdr ) )
    {
        List<FileSystemDto>? list = jss.Deserialize< List<FileSystemDto> >( jsonRdr );
        // TODO: Throw if `list` is null.
        return list;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接