如何在自定义的 System.Text.Json JsonConverter 中使用默认序列化?

25
我正在编写一个自定义的System.Text.Json.JsonConverter<T>,用于将旧数据模型升级到新版本。 我已经重写了Read()并实现了必要的后处理。 然而,在Write()方法中,我不需要进行任何自定义操作。 我怎样才能自动生成默认序列化,就好像我没有使用转换器一样? 显然,我可以只为反序列化和序列化使用不同的JsonSerializerOptions,但是我的框架不能直接为每个选项提供不同的选项。
以下是一个简化的示例。 假设我以前有以下数据模型:
public record Person(string Name);

我已经升级了

public record Person(string FirstName, string LastName);

我已经写了一个转换器,如下所示:

public sealed class PersonConverter : JsonConverter<Person>
{
    record PersonDTO(string FirstName, string LastName, string Name); // A DTO with both the old and new properties.

    public override Person Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var dto = JsonSerializer.Deserialize<PersonDTO>(ref reader, options);
        var oldNames = dto?.Name?.Split(' ', StringSplitOptions.RemoveEmptyEntries) ?? Enumerable.Empty<string>();
        return new Person(dto.FirstName ?? oldNames.FirstOrDefault(), dto.LastName ?? oldNames.LastOrDefault());
    }

    public override void Write(Utf8JsonWriter writer, Person person, JsonSerializerOptions options)
        => // What do I do here? I want to preserve other options such as options.PropertyNamingPolicy, which are lost by the following call
        JsonSerializer.Serialize(writer, person);
}

往返于

var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    Converters = { new PersonConverter() },
};
var person = JsonSerializer.Deserialize<Person>(json, options);
var json2 = JsonSerializer.Serialize(person, options);

那么结果就是{"FirstName":"FirstName","LastName":"LastName"} -- 也就是说,在序列化期间的驼峰式大小写被丢失了。但是,如果我在递归调用时传入选项进行编写,则情况会有所不同。

    public override void Write(Utf8JsonWriter writer, Person person, JsonSerializerOptions options)
        => // What do I do here? I want to preserve other options such as options.PropertyNamingPolicy, which are lost by the following call
        JsonSerializer.Serialize(writer, person, options);

然后序列化失败,并出现堆栈溢出。

我如何获得一个忽略自定义转换器的精确默认序列化?没有等同于Json.NET的 JsonConverter.CanWrite 属性。

演示fiddle 在此处

2个回答

18

文档中所解释的,转换器的选择具有以下优先级:

  • 应用于属性的[JsonConverter]
  • 添加到Converters集合中的转换器。
  • 应用于自定义值类型或POCO的[JsonConverter]

此外还有另一种情况:

  • 当工厂通过上述三种方法之一应用时,由某些JsonConverterFactory返回的JsonConverter<T>

每种情况都需要分别处理。

如果你应用了 [JsonConverter] 到一个属性上,那么简单地调用 JsonSerializer.Serialize(writer, person, options); 将会生成一个默认的序列化。
如果你向 Converters 集合中添加了一个转换器,则在 Write()(或Read())方法内,可以使用 JsonSerializerOptions复制构造函数 复制传入的 options, 从复制品的Converters列表中移除转换器,并将修改后的选项传递给 JsonSerializer.Serialize<T>(Utf8JsonWriter, T, JsonSerializerOptions)
在.NET Core 3.x 中无法轻松完成此操作,因为该版本中不存在复制构造函数。暂时修改传入选项的 Converters 集合以删除转换器不是线程安全的,因此不建议这样做。相反,需要创建新选项并手动复制每个属性以及Converters集合,跳过类型为converterType的转换器。
请注意,这将导致递归类型(如树)的序列化问题,因为同一类型的嵌套对象最初不会使用转换器进行序列化。
如果你将[JsonConverter] 应用于自定义值类型或 POCO,则似乎没有办法生成默认序列化。
本答案未涉及转换器列表中的 JsonConverterFactory 返回的 JsonConverter<T> 的情况,因为需要禁用工厂而不仅仅是转换器。在这种情况下,不清楚是否应完全禁用工厂,还是只针对特定的具体类型T禁用它。

由于在问题中,转换器已添加到Converters列表中,因此以下修改版本可以正确生成默认序列化:

public sealed class PersonConverter : DefaultConverterFactory<Person>
{
    record PersonDTO(string FirstName, string LastName, string Name); // A DTO with both the old and new properties.

    protected override Person Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions modifiedOptions)
    {
        var dto = JsonSerializer.Deserialize<PersonDTO>(ref reader, modifiedOptions);
        var oldNames = dto?.Name?.Split(' ', StringSplitOptions.RemoveEmptyEntries) ?? Enumerable.Empty<string>();
        return new Person(dto.FirstName ?? oldNames.FirstOrDefault(), dto.LastName ?? oldNames.LastOrDefault());
    }
}

public abstract class DefaultConverterFactory<T> : JsonConverterFactory
{
    class DefaultConverter : JsonConverter<T>
    {
        readonly JsonSerializerOptions modifiedOptions;
        readonly DefaultConverterFactory<T> factory;

        public DefaultConverter(JsonSerializerOptions options, DefaultConverterFactory<T> factory)
        {
            this.factory = factory;
            this.modifiedOptions = options.CopyAndRemoveConverter(factory.GetType());
        }

        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => factory.Write(writer, value, modifiedOptions);

        public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => factory.Read(ref reader, typeToConvert, modifiedOptions);
    }

    protected virtual T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions modifiedOptions)
        => (T)JsonSerializer.Deserialize(ref reader, typeToConvert, modifiedOptions);

    protected virtual void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions modifiedOptions) 
        => JsonSerializer.Serialize(writer, value, modifiedOptions);

    public override bool CanConvert(Type typeToConvert) => typeof(T) == typeToConvert;

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => new DefaultConverter(options, this);
}

public static class JsonSerializerExtensions
{
    public static JsonSerializerOptions CopyAndRemoveConverter(this JsonSerializerOptions options, Type converterType)
    {
        var copy = new JsonSerializerOptions(options);
        for (var i = copy.Converters.Count - 1; i >= 0; i--)
            if (copy.Converters[i].GetType() == converterType)
                copy.Converters.RemoveAt(i);
        return copy;
    }
}

注释:

  • 我使用了转换器工厂而不是转换器作为PersonConverter的基类,因为它允许我方便地缓存制造的转换器中复制的选项。

  • 如果您尝试将DefaultConverterFactory<T>应用于自定义值类型或POCO,例如:

    [JsonConverter(typeof(PersonConverter))] public record Person(string FirstName, string LastName);
    

    会发生一个令人讨厌的堆栈溢出。

演示代码片段请点击这里


1
这个答案的早期版本DefaultConverter的构造函数中缓存了JsonConverter<T> defaultConverter,但我已经将其删除,因为它会导致声明为object的值的多态序列化、对NumberHandling的支持以及编译时代码生成的问题,这些问题在这里报告。 - dbc
2
@PharazFadaei - 当转换器是由转换器工厂制造时,它不会添加到 JsonSerializerOptions.Converters 中。相反,它会被缓存在元数据信息 JsonSerializerOptions._classes 中。你可以在这个 traceback 中看到它: https://dotnetfiddle.net/hy94kw... - dbc
2
这就是问题所在。您可以从集合中删除生成的转换器(它不在那里),但工厂仍然存在,并且将再次提供相同的生成的转换器。我认为微软所说的“添加到Converters集合中的转换器”也包括在Converters集合中由工厂生成的转换器。因此,在这种情况下,仅按照您的第二点建议删除转换器是不够的。 - Pharaz Fadaei
1
我不会从集合中删除生成的转换器。我会从复制选项的集合中删除工厂:options.CopyAndRemoveConverter(factory.GetType())。然后,我将修改后的选项传递到递归序列化调用中。无论如何,如果有什么问题并且您可以通过[fiddle]提供一个最小可重现示例,我很乐意查看。 - dbc
2
@PharazFadaei - 很久没有回复了,我澄清一下答案仅适用于简单的转换器,而不是工厂。 - dbc
显示剩余4条评论

3
我真的很想在类上使用JsonConverterAttribute,最后采用了一个继承的私有虚拟类的解决方案,正如this answer所建议的那样。这也避免了当存在相同类型的属性时(例如在树结构中)可能出现的任何问题。
下面的示例不使用问题中的Person示例,但这个思路可以应用到那种情况。
假设你有一个SomeDto,你想在序列化或反序列化过程中进行一些特殊处理:
[JsonConverter(typeof(SomeDtoJsonConverter))]
internal class SomeDto
{
    public string? P1 { get; set; }
    public int P2 { get; set; }
    public SomeDto? P3 { get; set; }
    // ...
}

然后,您可以创建一个JsonConverter,通过将对象复制到一个没有自定义JsonConverterDummy私有类中来绕过对自身的引用,并使用该虚拟类型/对象调用"默认"JsonSerializer
internal class SomeDtoJsonConverter : JsonConverter<SomeDto>
{
    public override SomeDto? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // ...

        // Call the default deserializer using the private type Dummy to avoid infinite recursion
        Dummy? dummy = JsonSerializer.Deserialize<Dummy?>(ref reader, options);
        SomeDto? someDto = MapFrom<Dummy, SomeDto>(dummy);

        // ...
        return someDto;
    }

    public override void Write(Utf8JsonWriter writer, SomeDto someDto, JsonSerializerOptions options)
    {
        // ...
        Dummy? dummy = MapFrom<SomeDto, Dummy>(someDto);
        // ...

        // Call the default serializer using the private type Dummy to avoid infinite recursion
        JsonSerializer.Serialize<Dummy?>(writer, dummy, options);
    }

    private class Dummy : SomeDto { }

    // Copy properties from source object to new instance of target object, for instance using reflection.
    private static TTarget? MapFrom<TSource, TTarget>(TSource? sourceObject) where TTarget : class, new()
    {
        if (sourceObject is null)
            return null;

        IEnumerable<PropertyInfo> sourceProperties = typeof(TSource).GetProperties().Where(prop => prop.CanRead);
        PropertyInfo[] targetProperties = typeof(TTarget).GetProperties().Where(prop => prop.CanWrite).ToArray();

        TTarget target = new TTarget();
        foreach (PropertyInfo sourceProperty in sourceProperties)
        {
            PropertyInfo? targetProperty = targetProperties.FirstOrDefault(prop => prop.Name == sourceProperty.Name);
            targetProperty?.SetValue(target, sourceProperty.GetValue(sourceObject));
        }
        return target;
    }
}

使用示例:

public class SomeDtoJsonConverterTest
{
    [Fact]
    public void Write_SerializesUsingDefaultSerializer()
    {
        SomeDto someDto = new SomeDto { P1 = "Hello", P2 = 42 };
        string json = JsonSerializer.Serialize(someDto);
        Assert.Contains("\"P1\":\"Hello\"", json);
        Assert.Contains("\"P2\":42", json);
        Assert.Contains("\"P3\":null", json);
    }

    [Fact]
    public void Read_DeserializesUsingDefaultDeserializer()
    {
        string json = "{ \"P1\":\"Hello\", \"P2\":42 }";
        SomeDto someDto = JsonSerializer.Deserialize<SomeDto>(json)!;
        Assert.Equal("Hello", someDto.P1);
        Assert.Equal(42, someDto.P2);
        Assert.Null(someDto.P3);
    }

    [Fact]
    public void Write_SerializesTypeWithRecursiveProperty()
    {
        SomeDto someDto = new SomeDto { P1 = "Hello", P2 = 42 };
        someDto.P3 = new SomeDto { P1 = "World", P2 = 17 };
        string json = JsonSerializer.Serialize(someDto);
        Assert.Contains("\"P1\":\"Hello\"", json);
        Assert.Contains("\"P2\":42", json);
        Assert.Contains("\"P3\":{", json);
        Assert.Contains("\"P1\":\"World\"", json);
        Assert.Contains("\"P2\":17", json);
    }

    [Fact]
    public void Write_DeserializesTypeWithRecursiveProperty()
    {
        string json = "{ \"P1\":\"Hello\", \"P2\":42, \"P3\":{ \"P1\":\"World\", \"P2\":17 } }";
        SomeDto someDto = JsonSerializer.Deserialize<SomeDto>(json)!;
        Assert.Equal("Hello", someDto.P1);
        Assert.Equal(42, someDto.P2);
        Assert.Equal("World", someDto.P3!.P1);
        Assert.Equal(17, someDto.P3!.P2);
    }
}

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