System.Text.Json 中是否可以进行多态反序列化?

Sky*_*orm 60 c# json .net-core-3.0 system.text.json

我尝试从 Newtonsoft.Json 迁移到 System.Text.Json。我想反序列化抽象类。Newtonsoft.Json 为此具有 TypeNameHandling。有没有办法通过.net core 3.0 上的 System.Text.Json 反序列化抽象类?

ahs*_*han 60

System.Text.Json 中是否可以进行多态反序列化?

答案是肯定的否定的,这取决于您所说的“可能”是什么意思。

没有多态的反序列化(相当于Newtonsoft.Json的TypeNameHandling)支持内置System.Text.Json。这是因为阅读指定为JSON有效载荷(如在一个字符串的.NET类型名称$type是元数据属性)来创建你的对象不推荐使用,因为它引入了潜在的安全隐患(见https://github.com/dotnet/corefx/问题/41347#issuecomment-535779492了解更多信息)。

允许负载指定自己的类型信息是 Web 应用程序中常见的漏洞来源。

然而,通过创建一个以增加自己的多态反序列化方式支持JsonConverter<T>,所以在这个意义上说,这是可能的。

文档显示了如何使用类型鉴别器属性执行此操作的示例:https : //docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to#support-多态反序列化

让我们看一个例子。

假设您有一个基类和几个派生类:

public class BaseClass
{
    public int Int { get; set; }
}
public class DerivedA : BaseClass
{
    public string Str { get; set; }
}
public class DerivedB : BaseClass
{
    public bool Bool { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

您可以创建以下内容JsonConverter<BaseClass>,在序列化时写入类型鉴别器并读取它以确定要反序列化的类型。您可以在JsonSerializerOptions.

public class BaseClassConverter : JsonConverter<BaseClass>
{
    private enum TypeDiscriminator
    {
        BaseClass = 0,
        DerivedA = 1,
        DerivedB = 2
    }

    public override bool CanConvert(Type type)
    {
        return typeof(BaseClass).IsAssignableFrom(type);
    }

    public override BaseClass Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException();
        }

        if (!reader.Read()
                || reader.TokenType != JsonTokenType.PropertyName
                || reader.GetString() != "TypeDiscriminator")
        {
            throw new JsonException();
        }

        if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
        {
            throw new JsonException();
        }

        BaseClass baseClass;
        TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
        switch (typeDiscriminator)
        {
            case TypeDiscriminator.DerivedA:
                if (!reader.Read() || reader.GetString() != "TypeValue")
                {
                    throw new JsonException();
                }
                if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException();
                }
                baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));
                break;
            case TypeDiscriminator.DerivedB:
                if (!reader.Read() || reader.GetString() != "TypeValue")
                {
                    throw new JsonException();
                }
                if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException();
                }
                baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));
                break;
            default:
                throw new NotSupportedException();
        }

        if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
        {
            throw new JsonException();
        }

        return baseClass;
    }

    public override void Write(
        Utf8JsonWriter writer,
        BaseClass value,
        JsonSerializerOptions options)
    {
        writer.WriteStartObject();

        if (value is DerivedA derivedA)
        {
            writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedA);
            writer.WritePropertyName("TypeValue");
            JsonSerializer.Serialize(writer, derivedA);
        }
        else if (value is DerivedB derivedB)
        {
            writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);
            writer.WritePropertyName("TypeValue");
            JsonSerializer.Serialize(writer, derivedB);
        }
        else
        {
            throw new NotSupportedException();
        }

        writer.WriteEndObject();
    }
}
Run Code Online (Sandbox Code Playgroud)

这就是序列化和反序列化的样子(包括与 Newtonsoft.Json 的比较):

private static void PolymorphicSupportComparison()
{
    var objects = new List<BaseClass> { new DerivedA(), new DerivedB() };

    // Using: System.Text.Json
    var options = new JsonSerializerOptions
    {
        Converters = { new BaseClassConverter() },
        WriteIndented = true
    };

    string jsonString = JsonSerializer.Serialize(objects, options);
    Console.WriteLine(jsonString);
    /*
     [
      {
        "TypeDiscriminator": 1,
        "TypeValue": {
            "Str": null,
            "Int": 0
        }
      },
      {
        "TypeDiscriminator": 2,
        "TypeValue": {
            "Bool": false,
            "Int": 0
        }
      }
     ]
    */

    var roundTrip = JsonSerializer.Deserialize<List<BaseClass>>(jsonString, options);


    // Using: Newtonsoft.Json
    var settings = new Newtonsoft.Json.JsonSerializerSettings
    {
        TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,
        Formatting = Newtonsoft.Json.Formatting.Indented
    };

    jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(objects, settings);
    Console.WriteLine(jsonString);
    /*
     [
      {
        "$type": "PolymorphicSerialization.DerivedA, PolymorphicSerialization",
        "Str": null,
        "Int": 0
      },
      {
        "$type": "PolymorphicSerialization.DerivedB, PolymorphicSerialization",
        "Bool": false,
        "Int": 0
      }
     ]
    */

    var originalList = JsonConvert.DeserializeObject<List<BaseClass>>(jsonString, settings);

    Debug.Assert(originalList[0].GetType() == roundTrip[0].GetType());
}
Run Code Online (Sandbox Code Playgroud)

这是另一个 StackOverflow 问题,它展示了如何使用接口(而不是抽象类)支持多态反序列化,但类似的解决方案适用于任何多态: 是否有一种简单的方法可以在 System.Text 中的自定义转换器中手动序列化/反序列化子对象.Json?

  • @HerSta,阅读器是一个结构,因此您可以创建一个本地副本以返回到之前的状态或“重置”它。因此,您可以通过在副本上的循环中完全读取子对象来查找鉴别器值,然后在完成后更新转换器的输入参数,以便解串器知道您已读取整个对象以及读取位置继续阅读。 (4认同)
  • 如果我希望鉴别器成为对象的一部分怎么办?现在,这将“DerivedA”嵌套在“TypeValue”对象中。 (3认同)
  • 如果鉴别器值不是 json 中的第一个属性,如何重置读取器? (3认同)

dbc*_*dbc 51

白名单继承类型的多态序列化已在 .NET 7 中实现,并且在Preview 6中可用。

\n

从文档页面What\xe2\x80\x99s new in System.Text.Json in .NET 7: Type Hierarchies

\n
\n

System.Text.Json 现在支持用户定义类型层次结构的多态序列化和反序列化。这可以通过用新的 装饰类型层次结构的基类来启用JsonDerivedTypeAttribute

\n
\n

首先,让我们考虑序列化。假设您有以下类型层次结构:

\n
public abstract class BaseType { } // Properties omitted\n\npublic class DerivedType1 : BaseType { public string Derived1 { get; set; } } \npublic class DerivedType2 : BaseType { public int Derived2 { get; set; } }\n
Run Code Online (Sandbox Code Playgroud)\n

并且您有一个数据模型,其中包含声明类型为 的值BaseType,例如

\n
var list = new List<BaseType> { new DerivedType1 { Derived1 = "value 1" } };\n
Run Code Online (Sandbox Code Playgroud)\n

在以前的版本中,System.Text.Json 只会序列化声明的 type 的属性BaseType。现在,您将能够DerivedType1在序列化声明为的值时包含以下属性:BaseType通过添加[JsonDerivedType(typeof(TDerivedType))]BaseType所有派生类型:

\n
[JsonDerivedType(typeof(DerivedType1))]\n[JsonDerivedType(typeof(DerivedType2))]\npublic abstract class BaseType { } // Properties omitted\n
Run Code Online (Sandbox Code Playgroud)\n

以这种方式列入白名单后DerivedType1,模型的序列化:

\n
var json = JsonSerializer.Serialize(list);\n
Run Code Online (Sandbox Code Playgroud)\n

结果是

\n
[{"Derived1" : "value 1"}]\n
Run Code Online (Sandbox Code Playgroud)\n

演示小提琴#1在这里

\n

请注意,只有通过属性(或通过JsonTypeInfo.PolymorphismOptions运行时设置)列入白名单的派生类型才能通过此机制进行序列化。如果您有其他未列入白名单的派生类型,例如:

\n
public class DerivedType3 : BaseType { public string Derived3 { get; set; } } \n
Run Code Online (Sandbox Code Playgroud)\n

然后JsonSerializer.Serialize(new BaseType [] { new DerivedType3 { Derived3 = "value 3" } })会抛出System.NotSupportedException: Runtime type \'DerivedType3\' is not supported by polymorphic type \'BaseType\'异常。演示小提琴#2在这里

\n

这涵盖了序列化。如果需要往返类型层次结构,则需要提供用于每个派生类型的类型鉴别器属性值。JsonDerivedTypeAttribute.TypeDiscriminator这可以通过为每个派生类型提供一个值来完成:

\n
[JsonDerivedType(typeof(DerivedType1), "DerivedType1")]\n[JsonDerivedType(typeof(DerivedType2), "DerivedType2")]\npublic abstract class BaseType { } // Properties omitted\n
Run Code Online (Sandbox Code Playgroud)\n

现在当你序列化你的模型时

\n
var json = JsonSerializer.Serialize(list);\n
Run Code Online (Sandbox Code Playgroud)\n

System.Text.Json 将添加一个人工类型鉴别器属性"$type",指示序列化的类型:

\n
[{"$type" : "DerivedType1", "Derived1" : "value 1"}]\n
Run Code Online (Sandbox Code Playgroud)\n

完成此操作后,您现在可以反序列化数据模型,如下所示:

\n
var list2 = JsonSerializer.Deserialize<List<BaseType>>(json);\n
Run Code Online (Sandbox Code Playgroud)\n

并且序列化的实际具体类型将被保留。演示小提琴#3在这里

\n

还可以通过Contract Customization在运行时告知 System.Text.Json 您的类型层次结构。当您的类型层次结构无法修改时,或者某些派生类型位于不同的程序集中并且无法在编译时引用时,或者您尝试在多个旧序列化程序之间进行互操作时,您可能需要执行此操作。这里的基本工作流程是实例化 的实例并添加一个修饰符,该修饰符为您的基本类型设置必要的内容。DefaultJsonTypeInfoResolverPolymorphismOptionsJsonTypeInfo

\n

例如,BaseType可以在运行时启用层次结构的多态序列化,如下所示:

\n
public abstract class BaseType { } // Properties omitted\n\npublic class DerivedType1 : BaseType { public string Derived1 { get; set; } } \npublic class DerivedType2 : BaseType { public int Derived2 { get; set; } }\n
Run Code Online (Sandbox Code Playgroud)\n

演示小提琴#4在这里

\n

笔记:

\n
    \n
  1. 白名单方法与数据契约序列化器的方法一致,后者使用KnownTypeAttribute、 ,并且XmlSerializer使用XmlIncludeAttribute。它与 Json.NET 不一致,Json.NETTypeNameHandling序列化所有类型的类型信息,除非通过序列化绑定器显式过滤。

    \n

    仅允许反序列化白名单类型可防止13 号星期五:JSON 攻击类型注入攻击,包括Newtonsoft Json 中的 TypeNameHandling 警告外部 json 由于 Json.Net TypeNameHandling auto? 中的 TypeNameHandling 警告中详细介绍的攻击?

    \n
  2. \n
  3. 整数和字符串都可以用于类型鉴别器名称。如果您按如下方式定义类型层次结构:

    \n
    [JsonDerivedType(typeof(DerivedType1), 1)]\n[JsonDerivedType(typeof(DerivedType2), 2)]\npublic abstract class BaseType { } // Properties omitted\n
    Run Code Online (Sandbox Code Playgroud)\n

    然后序列化上面的列表结果

    \n
    var list = new List<BaseType> { new DerivedType1 { Derived1 = "value 1" } };\n
    Run Code Online (Sandbox Code Playgroud)\n

    然而,Newtonsoft 不使用数字类型鉴别器值,因此如果您与旧序列化器进行互操作,您可能希望避免这种情况。

    \n
  4. \n
  5. 默认类型鉴别器属性名称"$type"与 Json.NET 使用的类型鉴别器名称相同。如果您希望使用不同的属性名称(例如"__type"所使用的名称) DataContractJsonSerializer,请应用于JsonPolymorphicAttribute基本类型并按TypeDiscriminatorPropertyName如下方式设置:

    \n
    [JsonPolymorphic(TypeDiscriminatorPropertyName = "__type")]\n[JsonDerivedType(typeof(DerivedType1), "DerivedType1")]\n[JsonDerivedType(typeof(DerivedType2), "DerivedType2")]\npublic abstract class BaseType { } // Properties omitted\n
    Run Code Online (Sandbox Code Playgroud)\n
  6. \n
  7. 如果您要与 Json.NET(或 DataContractJsonSerializer)进行互操作,则可以将 的值设置TypeDiscriminator为等于旧序列化程序使用的类型鉴别器值。

    \n
  8. \n
  9. 如果序列化程序遇到尚未列入白名单的派生类型,您可以通过设置JsonPolymorphicAttribute.UnknownDerivedTypeHandling为以下之一来控制其行为:

    \n
    \n
    \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n \n
    JsonUnknownDerivedTypeHandling价值意义
    序列化失败0未声明的运行时类型的对象将无法进行多态序列化。
    回退到基本类型1未声明的运行时类型的对象将回退到基本类型的序列化协定。
    回退到最近的祖先2未声明的运行时类型的对象将恢复为最近声明的祖先类型的序列化协定。由于钻石模糊性限制,某些接口层次结构不受支持。
    \n
    \n
  10. \n
\n

  • 从 .NET 7 开始,这应该是当前的正确答案。 (4认同)
  • @Ewan - 对于多级多态类型层次结构,请参阅[如何在.NET 7 中使用 System.Text.Json 序列化多级多态类型层次结构?](/sf/ask/5222318601/) 。您需要手动将“[JsonDerivedType(typeof(DerivedDerivedModel), nameof(DerivedDerivedModel))]”添加到“DerivedModel”,或者编写自定义 TypeInfo 修饰符来自动执行此操作。 (2认同)

Dem*_*ski 15

我最终得到了那个解决方案。它是轻量级的,对我来说足够通用。

类型鉴别器转换器

public class TypeDiscriminatorConverter<T> : JsonConverter<T> where T : ITypeDiscriminator
{
    private readonly IEnumerable<Type> _types;

    public TypeDiscriminatorConverter()
    {
        var type = typeof(T);
        _types = AppDomain.CurrentDomain.GetAssemblies()
            .SelectMany(s => s.GetTypes())
            .Where(p => type.IsAssignableFrom(p) && p.IsClass && !p.IsAbstract)
            .ToList();
    }

    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException();
        }

        using (var jsonDocument = JsonDocument.ParseValue(ref reader))
        {
            if (!jsonDocument.RootElement.TryGetProperty(nameof(ITypeDiscriminator.TypeDiscriminator), out var typeProperty))
            {
                throw new JsonException();
            }

            var type = _types.FirstOrDefault(x => x.Name == typeProperty.GetString());
            if (type == null)
            {
                throw new JsonException();
            }

            var jsonObject = jsonDocument.RootElement.GetRawText();
            var result = (T) JsonSerializer.Deserialize(jsonObject, type, options);

            return result;
        }
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        JsonSerializer.Serialize(writer, (object)value, options);
    }
}
Run Code Online (Sandbox Code Playgroud)

界面

public interface ITypeDiscriminator
{
    string TypeDiscriminator { get; }
}
Run Code Online (Sandbox Code Playgroud)

和示例模型

public interface ISurveyStepResult : ITypeDiscriminator
{
    string Id { get; set; }
}

public class BoolStepResult : ISurveyStepResult
{
    public string Id { get; set; }
    public string TypeDiscriminator => nameof(BoolStepResult);

    public bool Value { get; set; }
}

public class TextStepResult : ISurveyStepResult
{
    public string Id { get; set; }
    public string TypeDiscriminator => nameof(TextStepResult);

    public string Value { get; set; }
}

public class StarsStepResult : ISurveyStepResult
{
    public string Id { get; set; }
    public string TypeDiscriminator => nameof(StarsStepResult);

    public int Value { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

这是测试方法

public void SerializeAndDeserializeTest()
    {
        var surveyResult = new SurveyResultModel()
        {
            Id = "id",
            SurveyId = "surveyId",
            Steps = new List<ISurveyStepResult>()
            {
                new BoolStepResult(){ Id = "1", Value = true},
                new TextStepResult(){ Id = "2", Value = "some text"},
                new StarsStepResult(){ Id = "3", Value = 5},
            }
        };

        var jsonSerializerOptions = new JsonSerializerOptions()
        {
            Converters = { new TypeDiscriminatorConverter<ISurveyStepResult>()},
            WriteIndented = true
        };
        var result = JsonSerializer.Serialize(surveyResult, jsonSerializerOptions);

        var back = JsonSerializer.Deserialize<SurveyResultModel>(result, jsonSerializerOptions);

        var result2 = JsonSerializer.Serialize(back, jsonSerializerOptions);
        
        Assert.IsTrue(back.Steps.Count == 3 
                      && back.Steps.Any(x => x is BoolStepResult)
                      && back.Steps.Any(x => x is TextStepResult)
                      && back.Steps.Any(x => x is StarsStepResult)
                      );
        Assert.AreEqual(result2, result);
    }
Run Code Online (Sandbox Code Playgroud)

  • 我不喜欢用类型鉴别器属性“污染”我的模型,但这是一个很好的解决方案,可以在“System.Text.Json”允许的范围内工作 (2认同)

小智 8

目前,借助 .NET 7 的新功能,我们无需编写方便的代码即可实现此目的。请参阅此处:https ://devblogs.microsoft.com/dotnet/announcing-dotnet-7-preview-5/

[JsonDerivedType(typeof(Derived1), 0)]
[JsonDerivedType(typeof(Derived2), 1)]
[JsonDerivedType(typeof(Derived3), 2)]
public class Base { }

JsonSerializer.Serialize<Base>(new Derived2()); // { "$type" : 1, ... }
Run Code Online (Sandbox Code Playgroud)

我希望这可以帮助你


Mic*_*iti 6

请试试我写的这个库作为 System.Text.Json 的扩展来提供多态性:https : //github.com/dahomey-technologies/Dahomey.Json

如果引用实例的实际类型与声明的类型不同,则 discriminator 属性将自动添加到输出 json 中:

public class WeatherForecast
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string Summary { get; set; }
}

public class WeatherForecastDerived : WeatherForecast
{
    public int WindSpeed { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

继承类必须手动注册到鉴别器约定注册表,以便让框架知道鉴别器值和类型之间的映射:

JsonSerializerOptions options = new JsonSerializerOptions();
options.SetupExtensions();
DiscriminatorConventionRegistry registry = options.GetDiscriminatorConventionRegistry();
registry.RegisterType<WeatherForecastDerived>();

string json = JsonSerializer.Serialize<WeatherForecast>(weatherForecastDerived, options);
Run Code Online (Sandbox Code Playgroud)

结果:

{
  "$type": "Tests.WeatherForecastDerived, Tests",
  "Date": "2019-08-01T00:00:00-07:00",
  "TemperatureCelsius": 25,
  "Summary": "Hot",
  "WindSpeed": 35
}
Run Code Online (Sandbox Code Playgroud)


Mar*_*s.D 5

这是我的所有抽象类型的 JsonConverter:

        private class AbstractClassConverter : JsonConverter<object>
        {
            public override object Read(ref Utf8JsonReader reader, Type typeToConvert,
                JsonSerializerOptions options)
            {
                if (reader.TokenType == JsonTokenType.Null) return null;

                if (reader.TokenType != JsonTokenType.StartObject)
                    throw new JsonException("JsonTokenType.StartObject not found.");

                if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName
                                   || reader.GetString() != "$type")
                    throw new JsonException("Property $type not found.");

                if (!reader.Read() || reader.TokenType != JsonTokenType.String)
                    throw new JsonException("Value at $type is invalid.");

                string assemblyQualifiedName = reader.GetString();

                var type = Type.GetType(assemblyQualifiedName);
                using (var output = new MemoryStream())
                {
                    ReadObject(ref reader, output, options);
                    return JsonSerializer.Deserialize(output.ToArray(), type, options);
                }
            }

            private void ReadObject(ref Utf8JsonReader reader, Stream output, JsonSerializerOptions options)
            {
                using (var writer = new Utf8JsonWriter(output, new JsonWriterOptions
                {
                    Encoder = options.Encoder,
                    Indented = options.WriteIndented
                }))
                {
                    writer.WriteStartObject();
                    var objectIntend = 0;

                    while (reader.Read())
                    {
                        switch (reader.TokenType)
                        {
                            case JsonTokenType.None:
                            case JsonTokenType.Null:
                                writer.WriteNullValue();
                                break;
                            case JsonTokenType.StartObject:
                                writer.WriteStartObject();
                                objectIntend++;
                                break;
                            case JsonTokenType.EndObject:
                                writer.WriteEndObject();
                                if(objectIntend == 0)
                                {
                                    writer.Flush();
                                    return;
                                }
                                objectIntend--;
                                break;
                            case JsonTokenType.StartArray:
                                writer.WriteStartArray();
                                break;
                            case JsonTokenType.EndArray:
                                writer.WriteEndArray();
                                break;
                            case JsonTokenType.PropertyName:
                                writer.WritePropertyName(reader.GetString());
                                break;
                            case JsonTokenType.Comment:
                                writer.WriteCommentValue(reader.GetComment());
                                break;
                            case JsonTokenType.String:
                                writer.WriteStringValue(reader.GetString());
                                break;
                            case JsonTokenType.Number:
                                writer.WriteNumberValue(reader.GetInt32());
                                break;
                            case JsonTokenType.True:
                            case JsonTokenType.False:
                                writer.WriteBooleanValue(reader.GetBoolean());
                                break;
                            default:
                                throw new ArgumentOutOfRangeException();
                        }
                    }
                }
            }

            public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
            {
                writer.WriteStartObject();
                var valueType = value.GetType();
                var valueAssemblyName = valueType.Assembly.GetName();
                writer.WriteString("$type", $"{valueType.FullName}, {valueAssemblyName.Name}");

                var json = JsonSerializer.Serialize(value, value.GetType(), options);
                using (var document = JsonDocument.Parse(json, new JsonDocumentOptions
                {
                    AllowTrailingCommas = options.AllowTrailingCommas,
                    MaxDepth = options.MaxDepth
                }))
                {
                    foreach (var jsonProperty in document.RootElement.EnumerateObject())
                        jsonProperty.WriteTo(writer);
                }

                writer.WriteEndObject();
            }

            public override bool CanConvert(Type typeToConvert) => 
                typeToConvert.IsAbstract && !EnumerableInterfaceType.IsAssignableFrom(typeToConvert);
        }
Run Code Online (Sandbox Code Playgroud)

  • @marcus-d 添加允许的程序集和/或类型的列表并检查“Read()”中的“$type”标记是否足够? (2认同)

归档时间:

查看次数:

25108 次

最近记录:

4 年,3 月 前