Custom serializer for just one property in Json.NET, without changing the model class

随声附和 提交于 2021-01-27 20:25:02

问题


I need to do something like the following, but I need to do it without putting an attribute on or otherwise polluting the model class. An ideal solution would work via the JsonSerializerSettings, without disturbing other custom serialization. Incidentally, the below came from this question: Custom conversion of specific objects in 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.
}

The JsonConverter for this toy example (content is not really relevant; what is relevant is that I use it for the property):

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

A passing unit test:

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

回答1:


You can programmatically apply a JsonConverter to one or more properties in a model class without using attributes via a custom ContractResolver. Here is a dirt simple example, which applies your AllCapsConverter to the LastName property in your Person class. (If you're looking for a more robust solution, have a look at @dbc's answer. My intent here was to show the simplest example that could possibly work.)

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

Here is the updated test and Person model which shows how to use the resolver:

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

Working demo: https://dotnetfiddle.net/o4e3WP




回答2:


You can apply converters to specific properties by using a custom IContractResolver inheriting from DefaultContractResolver.

First, grab ConfigurableContractResolver from this answer to How to add metadata to describe which properties are dates in 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
    // https://stackoverflow.com/a/46083201/3744182

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

Then, create a method to configure the JsonObjectContract for Person as follows:

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

And finally serialize as follows:

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

Notes:

  • Rather than creating ConfigurableContractResolver it would have been possible to subclass DefaultContractResolver, override DefaultContractResolver.CreateProperty, and hardcode the necessary logic for Person.LastName there. Creating a configurable resolver that allows for customizations to be combined in run time seems more useful and reuseable, however.

  • In AllCapsConverter.WriteJson() it would be simpler to use writer.WriteValue(string) to write your uppercase string:

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var upper = ((string)value).ToUpperInvariant();
        writer.WriteValue(upper);
    }
    
  • You may want to cache the contract resolver for best performance.

Sample fiddle here.



来源:https://stackoverflow.com/questions/53768041/custom-serializer-for-just-one-property-in-json-net-without-changing-the-model

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!