使用 json.net 反序列化没有类型信息的多态 json 类

Posted

技术标签:

【中文标题】使用 json.net 反序列化没有类型信息的多态 json 类【英文标题】:Deserializing polymorphic json classes without type information using json.net 【发布时间】:2013-10-18 22:05:43 【问题描述】:

此Imgur api 调用返回一个列表,其中包含以 JSON 表示的 Gallery ImageGallery Album 类。

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

This 问题似乎显示了一种方法,但它看起来有点像 hack。

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

样本数据:

图库图片


    "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 字符串并将其放入类中吗?我对there is no $type property 的意思感到困惑。 是的,我有 json 字符串并想反序列化为 C# 类。 Json.NET 似乎使用了一个名为 $type 的属性来区分数组中的不同类型。此数据没有该属性,仅使用“IsAlbum”属性。 【参考方案1】:

您可以通过创建自定义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

小提琴:https://dotnetfiddle.net/1kplME

【讨论】:

如果多态对象是递归的,即如果一个专辑可能包含其他专辑,这将不起作用。在转换器中,应该使用 Serializer.Populate() 而不是 item.ToObject()。见***.com/questions/29124126/… 对于尝试这种方法并发现它会导致无限循环(并最终导致堆栈溢出)的任何人,您可能希望使用Populate 方法而不是ToObject。查看***.com/questions/25404202/… 和***.com/questions/29124126/… 的答案。我在这里的 Gist 中有两种方法的示例:gist.github.com/chrisoldwood/b604d69543a5fe5896a94409058c7a95。 我在大约 8 小时内迷失在各种答案中,它们是关于 CustomCreationConverter 的。最后这个答案奏效了,我感到很开明。我的内部对象包含它的类型为字符串,我用它来进行这样的转换。 JObject item = JObject.Load(reader);类型类型 = Type.GetType(item["Type"].Value()); return item.ToObject(type); 这对我来说很好,前提是您没有将转换器属性放在基类上。转换器必须注入序列化器(通过设置等)并仅检查 CanConvert 中的基本类型。我正在使用 Populate() 进行辩论,但我真的不喜欢这两种方法。 我已将转换器修复为使用 JsonSerializer.Populate() 而不是 Ivan 和 Chris 建议的 JObject.ToObject()。这将避免递归循环的问题,并允许转换器成功地与属性一起使用。【参考方案2】:

只需使用适用于 Json.NET 的 JsonSubTypes 属性

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

【讨论】:

这应该是最佳答案。我花了一天的大部分时间来解决这个问题,检查来自数十位作者的自定义 JsonConverter 类。您的 nuget 包用三行代码代替了所有这些工作。干得好先生。干得好。 在 Java 世界中,Jackson 库通过@JsonSubTypes 属性提供了类似的支持。请参阅***.com/a/45447923/863980 以获取另一个用例示例(还有 @KonstantinPelepelin 在 cmets 中的 Cage/Animal 示例)。 这确实是最简单的答案,但不幸的是,它是以性能为代价的。我发现使用手写转换器反序列化速度快 2-3 倍(如@BrianRogers 的回答所示) 嗨@SvenVranckx,请随时在github.com/manuc66/JsonSubTypes 上打开一个问题 好主意,但不幸的是,这对我不起作用。我在 github 网站上尝试了各种建议的排列方式,但最后我需要使用手写的方法。【参考方案3】:

高级到Brian Rogers 答案。关于“使用 Serializer.Populate() 而不是 item.ToObject()”。 如果派生类型具有构造函数或它们的某些具有自己的自定义转换器,则必须使用通用方式来反序列化 JSON。 所以你必须把实例化新对象的工作留给 NewtonJson。这样你就可以在你的 CustomJsonConverter 中实现它:

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

如果你有对象的递归,这工作。

【讨论】:

这段代码在多线程上下文中是不安全的,因为默认的合约解析器会缓存合约。在合约上设置选项(如它的转换器)将导致它在并发调用甚至后续调用时表现不同。 serializer.ContractResolver.ResolveContract(DeterminedType) - 返回已经缓存的合约。所以contract.Converter = null;更改缓存对象。它只改变缓存对象的引用,并且是线程安全的。 这实际上是一个非常好的答案,它让一切变得非常简单。如果使用 Serializer.Populate(),则需要自己创建对象,所有创建对象的逻辑(例如 JsonConstructor 属性)都会被忽略。 当我的基本类型BaseDto[JsonConverter( typeof(MyConverter) )] 时,这对我有用,但是当我删除属性时(但仍在JsonSerializer.Converters 中指定MyConverter)它停止工作,因为contract.Converter 是总是 null。我不确定我应该如何向JsonSerializer 表明Dto 的子类也应该使用相同的转换器,嗯。 (不幸的是JsonConverter&lt;T&gt; 使CanConvert 方法sealed 啊!)【参考方案4】:

我发布这个只是为了消除一些混乱。如果您正在使用预定义的格式并需要对其进行反序列化,我发现这是最有效的方法,并演示了机制,以便其他人可以根据需要对其进行调整。

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] 属性反序列化? 我假设你可以简单地编辑全局 JsonFormatter 的设置来包含这个转换器。见***.com/questions/41629523/…【参考方案5】:

以下实现应该让您在不改变设计类的方式的情况下进行反序列化,并使用 $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();
    

【讨论】:

【参考方案6】:

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

(如果 JSON.NET 使用不可变类型来指示数据和配置何时不再可变,这将不是问题,le sigh

就我而言,我是这样做的:

    实现了自定义JsonConverter&lt;T&gt;(其中T 是我的DTO 的基类)。 定义了一个覆盖ResolveContractConverterDefaultContractResolver 子类,以为基类返回我的自定义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" );
    

假设我有一个 FileSystemDto 的 JSON 数组:

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

我希望 Json.net 将其反序列化为 List&lt;FileSystemDto&gt;...

所以定义DefaultContractResolver 的子类(或者如果你已经有一个解析器实现,那么子类(或compose))并覆盖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.
    

然后,要反序列化,请使用 JsonSerializer 实例并正确设置 ContractResolver,例如:

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;
    

【讨论】:

以上是关于使用 json.net 反序列化没有类型信息的多态 json 类的主要内容,如果未能解决你的问题,请参考以下文章

使用 Json.Net 序列化哈希表

使用JSON.NET反序列化json - 无法反序列化,因为类型需要一个JSON数组c#

使用 Json.Net 进行 C# 枚举反序列化:将值转换为类型时出错

Json.NET:将嵌套数组反序列化为强类型对象

使用来自 URL 的 JSON.NET 反序列化 JSON 数组

使用 System.Text.Json 反序列化匿名类型