问题
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 subclassDefaultContractResolver
, override DefaultContractResolver.CreateProperty, and hardcode the necessary logic forPerson.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