Пользовательский сериализатор только для одного свойства в Json.NET без изменения класса модели

2

Мне нужно сделать что-то вроде следующего, но мне нужно сделать это, не добавляя атрибут или иным образом не загрязняя класс модели. Идеальное решение будет работать через JsonSerializerSettings, не нарушая другие пользовательские сериализации. Кстати, нижеприведенный вопрос возник из этого вопроса: Пользовательское преобразование определенных объектов в JSON.NET

public class Person
{
    public string FirstName { get; set; }
    [JsonConverter(typeof(AllCapsConverter))]
    public string LastName { get; set; }
    // more properties here in the real example, some of which nest to properties that use their own JsonConverters.
}

JsonConverter для этого игрушечного примера (содержание на самом деле не имеет значения; важно то, что я использую его для свойства):

public class AllCapsConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
        => objectType == typeof(string);

    public override bool CanRead => false;

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotSupportedException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var str = value as string;
        var upper = str.ToUpperInvariant();
        JToken j = JToken.FromObject(upper);
        j.WriteTo(writer);
    }
}

Проходящий юнит-тест:

public class PersonSerializationTest
{
    [Fact]
    public void SerializePerson_LastNameCaps()
    {
        var person = new Person
        {
            FirstName = "George",
            LastName = "Washington"
        };
        var serialized = JsonConvert.SerializeObject(person);
        var expected = @"{""FirstName"":""George"",""LastName"":""WASHINGTON""}";
        Assert.Equal(expected, serialized);
    }
}
Теги:
serialization
json.net

2 ответа

1

Вы можете применять конвертеры к определенным свойствам с помощью пользовательского IContractResolver наследуемого от DefaultContractResolver.

Сначала возьмите ConfigurableContractResolver из этого ответа в разделе Как добавить метаданные, чтобы описать, какие свойства являются датами в JSON.Net:

public class ConfigurableContractResolver : DefaultContractResolver
{
    // This contract resolver taken from the answer to
    // https://stackoverflow.com/questions/46047308/how-to-add-metadata-to-describe-which-properties-are-dates-in-json-net
    // /questions/6886001/how-to-add-metadata-to-describe-which-properties-are-dates-in-jsonnet/13693525#13693525

    readonly object contractCreatedPadlock = new object();
    event EventHandler<ContractCreatedEventArgs> contractCreated;
    int contractCount = 0;

    void OnContractCreated(JsonContract contract, Type objectType)
    {
        EventHandler<ContractCreatedEventArgs> created;
        lock (contractCreatedPadlock)
        {
            contractCount++;
            created = contractCreated;
        }
        if (created != null)
        {
            created(this, new ContractCreatedEventArgs(contract, objectType));
        }
    }

    public event EventHandler<ContractCreatedEventArgs> ContractCreated
    {
        add
        {
            lock (contractCreatedPadlock)
            {
                if (contractCount > 0)
                {
                    throw new InvalidOperationException("ContractCreated events cannot be added after the first contract is generated.");
                }
                contractCreated += value;
            }
        }
        remove
        {
            lock (contractCreatedPadlock)
            {
                if (contractCount > 0)
                {
                    throw new InvalidOperationException("ContractCreated events cannot be removed after the first contract is generated.");
                }
                contractCreated -= value;
            }
        }
    }

    protected override JsonContract CreateContract(Type objectType)
    {
        var contract = base.CreateContract(objectType);
        OnContractCreated(contract, objectType);
        return contract;
    }
}

public class ContractCreatedEventArgs : EventArgs
{
    public JsonContract Contract { get; private set; }
    public Type ObjectType { get; private set; }

    public ContractCreatedEventArgs(JsonContract contract, Type objectType)
    {
        this.Contract = contract;
        this.ObjectType = objectType;
    }
}

public static class ConfigurableContractResolverExtensions
{
    public static ConfigurableContractResolver Configure(this ConfigurableContractResolver resolver, EventHandler<ContractCreatedEventArgs> handler)
    {
        if (resolver == null || handler == null)
            throw new ArgumentNullException();
        resolver.ContractCreated += handler;
        return resolver;
    }
}

Затем создайте метод для настройки JsonObjectContract for Person следующим образом:

public static class JsonContractExtensions
{
    public static void ConfigurePerson(this JsonContract contract)
    {
        if (!typeof(Person).IsAssignableFrom(contract.UnderlyingType))
            return;
        var objectContract = contract as JsonObjectContract;
        if (objectContract == null)
            return;
        var property = objectContract.Properties.Where(p => p.UnderlyingName == nameof(Person.LastName)).Single();
        property.Converter = new AllCapsConverter();
    }
}

И наконец сериализовать следующим образом:

// Cache the contract resolver statically for best performance.
var resolver = new ConfigurableContractResolver()
    .Configure((s, e) => { e.Contract.ConfigurePerson(); });

var settigs = new JsonSerializerSettings
{
    ContractResolver = resolver,
};

var person = new Person
{
    FirstName = "George",
    LastName = "Washington"
};
var serialized = JsonConvert.SerializeObject(person, settigs);

Заметки:

  • Вместо создания ConfigurableContractResolver было бы возможно создать подкласс DefaultContractResolver, переопределить DefaultContractResolver.CreateProperty и жестко закодировать необходимую логику для Person.LastName. Однако создание настраиваемого распознавателя, позволяющего объединять настройки во время выполнения, представляется более полезным и многократно используемым.

  • В AllCapsConverter.WriteJson() было бы проще использовать writer.WriteValue(string) для записи строки в верхнем регистре:

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var upper = ((string)value).ToUpperInvariant();
        writer.WriteValue(upper);
    }
    
  • Вы можете кэшировать распознаватель контрактов для лучшей производительности.

Образец скрипки здесь.

0

Вы можете программно применить JsonConverter к одному или нескольким свойствам в классе модели, не используя атрибуты через пользовательский ContractResolver. Вот простой пример, который применяет ваш AllCapsConverter к свойству LastName в вашем классе Person. (Если вы ищете более надежное решение, посмотрите на ответ @dbc. Я хотел показать самый простой пример, который мог бы сработать.)

public class CustomResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty prop = base.CreateProperty(member, memberSerialization);
        if (prop.DeclaringType == typeof(Person) && prop.UnderlyingName == "LastName")
        {
            prop.Converter = new AllCapsConverter();
        }
        return prop;
    }
}

Вот обновленный тест и модель Person которая показывает, как использовать распознаватель:

public class PersonSerializationTest
{
    [Fact]
    public void SerializePerson_LastNameCaps()
    {
        var person = new Person
        {
            FirstName = "George",
            LastName = "Washington"
        };
        var settings = new JsonSerializerSettings
        {
            ContractResolver = new CustomResolver()
        };
        var serialized = JsonConvert.SerializeObject(person, settings);
        var expected = @"{""FirstName"":""George"",""LastName"":""WASHINGTON""}";
        Assert.Equal(expected, serialized);
    }
}

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Рабочая демонстрация: https://dotnetfiddle.net/o4e3WP

Ещё вопросы

Сообщество Overcoder
Наверх
Меню