Как использовать сериализацию по умолчанию в настраиваемом System.Text.Json JsonConverter?

Я пишу custom System.Text.Json.JsonConverter, чтобы обновить старую модель данных до новой версии. Я переопределил Read() и реализовал необходимую постобработку. Однако мне вообще не нужно делать ничего настраиваемого в _ 3_ метод. Как я могу автоматически сгенерировать сериализацию по умолчанию, которую я получил бы, если бы у меня вообще не было конвертера? Очевидно, я мог бы просто использовать разные 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 в Json.NET.

Демо-скрипт здесь.


person dbc    schedule 23.12.2020    source источник


Ответы (1)


Как объясняется в docs, конвертеры выбираются со следующим приоритетом:

  • [JsonConverter] применяется к собственности.
  • Конвертер добавлен в коллекцию Converters.
  • [JsonConverter] применяется к пользовательскому типу значения или POCO.

Каждый случай нужно рассматривать отдельно.

  1. Если к свойству применено [JsonConverter]., то простой вызов JsonSerializer.Serialize(writer, person, options); приведет к сериализации по умолчанию.

  2. Если у вас есть преобразователь, добавленный в коллекцию Converters., то внутри метода Write() (или Read()) вы можете скопировать входящий options, используя _ 10_ копировальный конвертер, удалите копирующий конструктор список href = "https://docs.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions.converters?view=net-5.0" rel = "noreferrer"> _ 11_ , и передайте измененную копию в JsonSerializer.Serialize<T>(Utf8JsonWriter, T, JsonSerializerOptions);

    Это не так просто сделать в .NET Core 3.x, потому что конструктор копирования не существует в этой версии. Временное изменение коллекции Converters входящих параметров для удаления преобразователя не будет небезопасным для потоков и поэтому не рекомендуется. Вместо этого нужно было бы создать новые параметры и вручную скопировать каждое свойство, а также коллекцию Converters, пропуская преобразования типа converterType.

  3. Если вы [JsonConverter] применили к настраиваемому типу значения или POCO., похоже, нет способа сгенерировать сериализацию по умолчанию.

Поскольку в вопросе преобразователь добавлен в список 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, JsonConverter<Person> defaultConverter)
    {
        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;
        readonly JsonConverter<T> defaultConverter;
        
        public DefaultConverter(JsonSerializerOptions options, DefaultConverterFactory<T> factory)
        {
            this.factory = factory;
            this.modifiedOptions = options.CopyAndRemoveConverter(factory.GetType());
            this.defaultConverter = (JsonConverter<T>)modifiedOptions.GetConverter(typeof(T));
        }
    
        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => factory.Write(writer, value, modifiedOptions, defaultConverter);

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

    protected virtual T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions modifiedOptions, JsonConverter<T> defaultConverter)
        => defaultConverter.ReadOrSerialize<T>(ref reader, typeToConvert, modifiedOptions);

    protected virtual void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions modifiedOptions, JsonConverter<T> defaultConverter) 
        => defaultConverter.WriteOrSerialize(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;
    }
    
    public static void WriteOrSerialize<T>(this JsonConverter<T> converter, Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        if (converter != null)
            converter.Write(writer, value, options);
        else
            JsonSerializer.Serialize(writer, value, options);
    }
    
    public static T ReadOrSerialize<T>(this JsonConverter<T> converter, ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (converter != null)
            return converter.Read(ref reader, typeToConvert, options);
        else
            return (T)JsonSerializer.Deserialize(ref reader, typeToConvert, options);
    }
}

Примечания:

  • Я использовал фабрику конвертеров, а не конвертер в качестве базового класса для PersonConverter, потому что это позволяло мне удобно кэшировать скопированные параметры и конвертер по умолчанию внутри изготовленного конвертера.

  • Если вы попытаетесь применить DefaultConverterFactory<T> к пользовательскому типу значения или POCO, например

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

    Произойдет неприятное переполнение стека.

Демо-скрипт здесь.

person dbc    schedule 23.12.2020