Implementing ASP.NET Web API Optional Parameters - c#

I need the ability to distinguish between a key not being supplied and null.
An example of the JSON would be:
# key not specified
{}
# key specified but null
{'optionalKey' : null}
# key specified and is valid
{'optionalKey' : 123}
To distinguishable between a key's absence and null, I've created a generic Optional class which wraps each field, but this requires writing a custom JsonConverter and DefaultContractResolver to flatten the JSON / unpack the OptionalType (sending nested JSON for each field is not an option).
I've managed to create a LINQPad script to do this but I can't help but thinking there must be an easier way that doesn't involve reflection?
void Main()
{
//null
Settings settings = null;
JsonConvert.SerializeObject(settings, new JsonSerializerSettings() { ContractResolver = new ShouldSerializeContractResolver() }).Dump();
settings = new Settings();
// no key {}
settings.OptionalIntegerSetting = null;
JsonConvert.SerializeObject(settings, new JsonSerializerSettings() { ContractResolver = new ShouldSerializeContractResolver() }).Dump();
// null key {\"OptionalIntegerSetting\" : null}
settings.OptionalIntegerSetting = new Optional<uint?>(); // assigning this to null assigns the optional type class, it does not use the implict operators.
JsonConvert.SerializeObject(settings, new JsonSerializerSettings() { ContractResolver = new ShouldSerializeContractResolver() }).Dump();
// has value {\"OptionalIntegerSetting\" : 123}
settings.OptionalIntegerSetting = 123;
JsonConvert.SerializeObject(settings, new JsonSerializerSettings() { ContractResolver = new ShouldSerializeContractResolver() }).Dump();
JsonConvert.DeserializeObject<Settings>("{}").Dump();
JsonConvert.DeserializeObject<Settings>("{'OptionalIntegerSetting' : null}").Dump();
JsonConvert.DeserializeObject<Settings>("{'OptionalIntegerSetting' : '123'}").Dump(); // supplying 'a string' instead of '123' currently breaks OptionalConverter.ReadJson
}
public class Settings
{
public Optional<uint?> OptionalIntegerSetting { get; set; }
}
[JsonConverter(typeof(OptionalConverter))]
public class Optional<T>
{
public T Value { get; set; }
public Optional() { }
public Optional(T value)
{
Value = value;
}
public static implicit operator Optional<T>(T t)
{
return new Optional<T>(t);
}
public static implicit operator T(Optional<T> t)
{
return t.Value;
}
}
// Provides a way of populating the POCO Resource model with CanSerialise proerties at the point just before serialisation.
// This prevents having to define a CanSerialiseMyProperty method for each property.
public class ShouldSerializeContractResolver : DefaultContractResolver
{
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Optional<>))
{
// add an additional ShouldSerialize property to omit no json
property.ShouldSerialize = instance =>
instance.GetType().GetProperty(property.PropertyName).GetValue(instance) != null;
}
return property;
}
}
// Performs the conversion to and from a JSON value to compound type
public class OptionalConverter : JsonConverter
{
public override bool CanWrite => true;
public override bool CanRead => true;
public override bool CanConvert(Type objectType)
{
return objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Optional<>);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jtoken = JToken.Load(reader);
var genericTypeArgument = objectType.GetGenericArguments()[0];
var constructor = objectType.GetConstructor(new[] { genericTypeArgument });
var result = JTokenType.Null != jtoken.Type ? jtoken.ToObject(genericTypeArgument) : null;
return constructor.Invoke(new object[] { JTokenType.Null != jtoken.Type ? jtoken.ToObject(genericTypeArgument) : null });
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var val = value.GetType().GetProperty("Value").GetValue(value);
(val != null ? JValue.FromObject(val) : JValue.CreateNull()).WriteTo(writer);
}
}

Full credit goes to #dbc.
void Main()
{
var settings = new Settings();
// no key {}
settings.OptionalIntegerSetting = null;
JsonConvert.SerializeObject(settings).Dump();
// null key {\"OptionalIntegerSetting\" : null}
settings.OptionalIntegerSetting = null;
settings.OptionalIntegerSettingSpecified = true;
JsonConvert.SerializeObject(settings).Dump();
// has value {\"OptionalIntegerSetting\" : 123}
settings.OptionalIntegerSetting = 123;
JsonConvert.SerializeObject(settings).Dump();
JsonConvert.DeserializeObject<Settings>("{}").Dump();
JsonConvert.DeserializeObject<Settings>("{'OptionalIntegerSetting' : null}").Dump();
JsonConvert.DeserializeObject<Settings>("{'OptionalIntegerSetting' : '123'}").Dump();
}
public class Settings
{
public uint? OptionalIntegerSetting { get; set; }
[JsonIgnore]
public bool OptionalIntegerSettingSpecified { get; set;}
}

Related

System.Text.Json - Use custom JsonConverter conditionally depending on field attribute

I have a custom attribute [Foo]
implemented as follows:
public class FooAttribute
: Attribute
{
}
Now I want to use the System.Text.Json.JsonSerializer to step into each field that has that attribute, in order to manipulate how is serialized and deserialized.
For example, if I have the following class
class SampleInt
{
[Foo]
public int Number { get; init; }
public int StandardNumber { get; init; }
public string Text { get; init; }
}
when I serialize an instance of this class, I want a custom int JsonConverter to apply only for that field.
public class IntJsonConverter
: JsonConverter<int>
{
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// do whatever before reading if the text starts with "potato". But this should be triggered only if destination type has the Foo attribute. How?
return reader.GetInt32();
}
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
{
writer.WriteStringValue("potato" + value.ToString());
}
}
so that the serialization for
var sample =
new SampleInt
{
Number = 123,
StandardNumber = 456
Text = "bar"
};
like this
var serializeOptions = new JsonSerializerOptions();
var serializeOptions.Converters.Add(new IntJsonConverter());
var resultJson = JsonSerializer.Serialize(sample, serializeOptions);
results on the following json
{
"number": "potato123",
"standardNumber": 456,
"text": "bar"
}
and not in
{
"number": "potato123",
"standardNumber": "potato456",
"text": "bar"
}
In a similar manner, I want the deserialization to be conditional, and only use the custom converter if the destination field has the [Foo] attribute.
With Newtonsoft, this is possible using Contract Resolvers and overriding CreateProperties method like this.
public class SerializationContractResolver
: DefaultContractResolver
{
private readonly ICryptoTransform _encryptor;
private readonly FieldEncryptionDecryption _fieldEncryptionDecryption;
public SerializationContractResolver(
ICryptoTransform encryptor,
FieldEncryptionDecryption fieldEncryptionDecryption)
{
_encryptor = encryptor;
_fieldEncryptionDecryption = fieldEncryptionDecryption;
NamingStrategy = new CamelCaseNamingStrategy();
}
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
var properties = base.CreateProperties(type, memberSerialization);
foreach (var jsonProperty in properties)
{
var hasAttribute = HasAttribute(type, jsonProperty);
if (hasAttribute)
{
var serializationJsonConverter = new MyJsonConverter();
jsonProperty.Converter = serializationJsonConverter;
}
}
return properties;
}
private bool HasAttribute(Type type, JsonProperty jsonProperty)
{
var propertyInfo = type.GetProperty(jsonProperty.UnderlyingName);
if (propertyInfo is null)
{
return false;
}
var hasAttribute =
propertyInfo.CustomAttributes
.Any(x => x.AttributeType == typeof(FooAttribute));
var propertyType = propertyInfo.PropertyType;
var isSimpleValue = propertyType.IsValueType || propertyType == typeof(string);
var isSupportedField = isSimpleValue && hasPersonalDataAttribute;
return isSupportedField;
}
}
But I don't want to use Newtonsoft. I want to use the new dotnet System.Text.Json serializer. Is it possible to use it in a similar granular way?

How to serialize Task<TResult> with Json.NET?

Consider
public class InQuestion<TType>
{
// JsonConverter(typeof CustomConverter))
public Task<TType> toConvert { get; set; }
}
How can I (de)serialize this class with json.net?
What I think I actually want to serialize is the underlying Task< T >.Result, which can then be deserialized with Task< T >.FromResult(). If I am to use custom JsonConverter, I cannot pass generic TType through Attribute, to reconstruct (or retrieve) TType object in the JsonConverter. Hence I'm stuck.
Question came to be from this code:
public class Program
{
public class InQuestion<TType>
{
public Task<TType> toConvert { get; set; }
}
public class Result
{
public int value { get; set; }
}
public static async Task Main()
{
var questionable = new InQuestion<Result>();
questionable.toConvert = Task.Run(async () => new Result { value = 42 });
await questionable.toConvert;
string json = JsonConvert.SerializeObject(questionable);
Debug.WriteLine(json);
InQuestion<Result> back = JsonConvert.DeserializeObject(json, typeof(InQuestion<Result>)) as InQuestion<Result>;
Debug.Assert(back?.toConvert?.Result?.value == 42);
}
}
which, surprisingly to me, halts, during the call to JsonConvert.DeserializeObject. https://github.com/JamesNK/Newtonsoft.Json/issues/1886 talks about the issue and recommends reasonable "Don't ever serialize/deserialize task.", but doesn't actually advice how to serialize the underlying Task< T >.Result.
A Task is a promise of a future value, and of course you cannot serialize a value that has yet to be provided.
Because the InQuestion object holds a Task member, you cannot serialize and deserialize the InQuestion object.
The workaround is to serialize the result, and reconstruct the InQuestion object after deserialization.
public static async Task Main()
{
var questionable = new InQuestion<Result>();
questionable.toConvert = Task.Run(async () => new Result { value = 42 });
Result result = await questionable.toConvert;
string json = JsonConvert.SerializeObject(result);
Result back = JsonConvert.DeserializeObject(json, typeof<Result>) as Result;
InQuestion<Result> reconstructed = new InQuestion<Result>()
{
toConvert = Task.FromResult(back)
};
}
I have found two solutions to this problem.
From the Add support for generic JsonConverter instantiation:
[JsonConverter(typeof(InQuestionConverter<>))]
public class InQuestion<TResult>
{
public Task<TResult> toConvert { get; set; }
}
public class Result
{
public int value { get; set; }
public string text { get; set; }
public override bool Equals(object obj)
{
return obj is Result result &&
value == result.value &&
text == result.text;
}
}
public class InQuestionConverter<TResult> : JsonConverter<InQuestion<TResult>>
{
public override InQuestion<TResult> ReadJson(JsonReader reader, Type objectType, InQuestion<TResult> existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (hasExistingValue)
existingValue.toConvert = Task.FromResult(serializer.Deserialize<TResult>(reader));
else
existingValue = new InQuestion<TResult>
{
toConvert = Task.FromResult(serializer.Deserialize<TResult>(reader))
};
return existingValue;
}
public override void WriteJson(JsonWriter writer, InQuestion<TResult> value, JsonSerializer serializer)
{
serializer.Serialize(writer, value.toConvert.Result, typeof(TResult));
}
}
public sealed class CustomContractResolver : DefaultContractResolver
{
protected override JsonConverter ResolveContractConverter(Type objectType)
{
var typeInfo = objectType.GetTypeInfo();
if (typeInfo.IsGenericType && !typeInfo.IsGenericTypeDefinition)
{
var jsonConverterAttribute = typeInfo.GetCustomAttribute<JsonConverterAttribute>();
if (jsonConverterAttribute != null && jsonConverterAttribute.ConverterType.GetTypeInfo().IsGenericTypeDefinition)
{
Type t = jsonConverterAttribute.ConverterType.MakeGenericType(typeInfo.GenericTypeArguments);
object[] parameters = jsonConverterAttribute.ConverterParameters;
return (JsonConverter)Activator.CreateInstance(t, parameters);
}
}
return base.ResolveContractConverter(objectType);
}
}
public static void Main()
{
var questionable = new InQuestion<Result>();
questionable.toConvert = Task.Run(async () => { return new Result { value = 42, text = "fox" }; });
questionable.toConvert.Wait();
string json = JsonConvert.SerializeObject(questionable, Formatting.None, new JsonSerializerSettings { ContractResolver = new CustomContractResolver() });
InQuestion<Result> back = JsonConvert.DeserializeObject(json, typeof(InQuestion<Result>), new JsonSerializerSettings { ContractResolver = new CustomContractResolver() }) as InQuestion<Result>;
Debug.Assert(back.toConvert.Result.Equals(questionable.toConvert.Result));
return;
}
Enables a custom ContractResolver which will point to correct generic instantiation of JsonConverter<TResult>, in which serialization is straightforward. This requires configuring JsonSerializerSettings and providing serialization for the entire InQuestion class (note that converter doesn't check for Task.IsCompleted in this sample).
Alternatively, using JsonConverterAttribute just on properties of type Task<T> and relying on reflection to retrieve TResult type from non-generic Converter:
public class InQuestion<TResult>
{
[JsonConverter(typeof(FromTaskOfTConverter))]
public Task<TResult> toConvert { get; set; }
}
public class FromTaskOfTConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return IsDerivedFromTaskOfT(objectType);
}
static bool IsDerivedFromTaskOfT(Type type)
{
while (type.BaseType != typeof(object))
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>))
return true;
type = type.BaseType;
}
return false;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
Debug.Assert(IsDerivedFromTaskOfT(objectType));
Type TResult = objectType.GetGenericArguments()[0];
object ResultValue = serializer.Deserialize(reader, TResult);
return typeof(Task).GetMethod("FromResult").MakeGenericMethod(TResult).Invoke(null, new[] { ResultValue });
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
Type objectType = value.GetType();
Debug.Assert(IsDerivedFromTaskOfT(objectType));
Type TResult = objectType.GetGenericArguments()[0];
Type TaskOfTResult = typeof(Task<>).MakeGenericType(TResult);
if ((bool)TaskOfTResult.GetProperty("IsCompleted").GetValue(value) == true)
{
object ResultValue = TaskOfTResult.GetProperty("Result").GetValue(value);
serializer.Serialize(writer, ResultValue, TResult);
}
else
{
serializer.Serialize(writer, Activator.CreateInstance(TResult));
}
}
}
public static void Main()
{
var questionable = new InQuestion<Result>();
questionable.toConvert = Task.Run(async () => { return new Result { value = 42, text = "fox" }; });
questionable.toConvert.Wait();
string json = JsonConvert.SerializeObject(questionable);
InQuestion<Result> back = JsonConvert.DeserializeObject(json, typeof(InQuestion<Result>)) as InQuestion<Result>;
Debug.Assert(back.toConvert.Result.Equals(questionable.toConvert.Result));
return;
}
With all that, I won't mark this accepted, since I lack understanding in both generics reflection and json.net.

Json.NET not properly deserializing System.Range property

I have a class with a property as type System.Range. When I serialize the class to JSON the values are there and look correct. However, when I deserialize the JSON back into an object the Range is set to 0..0 and not the saved value.
Here is a simple example
public class Program {
public static void Main() {
var room = new Room() {
Name = "Hall",
Capacity = 100..150
};
var json = JsonConvert.SerializeObject(room);
json.Dump();
Console.WriteLine();
var deRoom = JsonConvert.DeserializeObject<Room>(json);
deRoom.Dump();
}
}
public class Room {
public string Name {get;set;}
public Range Capacity {get;set;}
}
This dumps the following:
{"Name":"Hall","Capacity":{"Start":{"Value":100,"IsFromEnd":false},"End":"Value":150,"IsFromEnd":false}}}
Dumping object(Room)
Capacity : 0..0
Name : Hall
https://dotnetfiddle.net/jfOBNm
The reason is that properties of Range are readonly. You can validate it adding tracewriter:
MemoryTraceWriter traceWriter = new MemoryTraceWriter();
var deRoom = JsonConvert.DeserializeObject<Room>(json, new JsonSerializerSettings {
TraceWriter = traceWriter,
});
// will output bunch of "Info Unable to deserialize value to non-writable property 'Value' on System.Index. Path 'Capacity.Start.Value', line 1, position 47."
Console.WriteLine(traceWriter.ToString());
You can implement your own JsonConverter to handle it. For example:
class RangeConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(Range));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject jObj = JObject.Load(reader);
int value = 0;
bool isFromEnd = false;
var startObject = jObj.GetValue("start", StringComparison.InvariantCultureIgnoreCase);
if (startObject != null) {
value = GetIntValue(startObject, "value");
isFromEnd = GetBoolValue(startObject, "isFromEnd");
}
var startIndex = new Index(value, isFromEnd);
var endObject = jObj.GetValue("end", StringComparison.InvariantCultureIgnoreCase);
if (endObject == null) {
value = 0;
isFromEnd = true;
} else {
value = GetIntValue(endObject, "value");
isFromEnd = GetBoolValue(endObject, "isFromEnd");
}
var endIndex = new Index(value, isFromEnd);
return new Range(startIndex, endIndex);
}
private int GetIntValue(JToken token, string propertyName) {
var intValue = ((JObject)token).GetValue(propertyName, StringComparison.InvariantCultureIgnoreCase);
return intValue == null ? 0 : intValue.Value<int>();
}
private bool GetBoolValue(JToken token, string propertyName) {
var boolValue = ((JObject)token).GetValue(propertyName, StringComparison.InvariantCultureIgnoreCase);
return boolValue != null && boolValue.Value<bool>();
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
var deRoom = JsonConvert.DeserializeObject<Room>(json, new JsonSerializerSettings {
Converters = { new RangeConverter() } // add RangeConverter
});
deRoom.Dump();

How to add metadata to describe which properties are dates in JSON.Net

I would like to add a metadata property to my json so that the client side can know what properties are dates.
For example if I had an object like this:
{
"notADate": "a value",
"aDate": "2017-04-23T18:25:43.511Z",
"anotherDate": "2017-04-23T18:25:43.511Z"
}
I would like to add a metadata property to tell the consumer which properties to treat as dates something like this:
{
"_date_properties_": ["aDate", "anotherDate"],
"notADate": "a value",
"aDate": "2017-04-23T18:25:43.511Z",
"anotherDate": "2017-04-23T18:25:43.511Z"
}
Any help would be great, thanks!
You could create a custom ContractResolver that inserts a synthetic "_date_properties_" property into the contract of every object that is serialized.
To do this, first subclass DefaultContractResolver to allow contracts to be fluently customized after they have been created by application-added event handlers:
public class ConfigurableContractResolver : DefaultContractResolver
{
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;
}
}
Next, create an extension method to add the desired property to a JsonObjectContract:
public static class JsonContractExtensions
{
const string DatePropertiesName = "_date_properties_";
public static void AddDateProperties(this JsonContract contract)
{
var objectContract = contract as JsonObjectContract;
if (objectContract == null)
return;
var properties = objectContract.Properties.Where(p => p.PropertyType == typeof(DateTime) || p.PropertyType == typeof(DateTime?)).ToList();
if (properties.Count > 0)
{
var property = new JsonProperty
{
DeclaringType = contract.UnderlyingType,
PropertyName = DatePropertiesName,
UnderlyingName = DatePropertiesName,
PropertyType = typeof(string[]),
ValueProvider = new FixedValueProvider(properties.Select(p => p.PropertyName).ToArray()),
AttributeProvider = new NoAttributeProvider(),
Readable = true,
Writable = false,
// Ensure // Ensure PreserveReferencesHandling and TypeNameHandling do not apply to the synthetic property.
ItemIsReference = false,
TypeNameHandling = TypeNameHandling.None,
};
objectContract.Properties.Insert(0, property);
}
}
class FixedValueProvider : IValueProvider
{
readonly object properties;
public FixedValueProvider(object value)
{
this.properties = value;
}
#region IValueProvider Members
public object GetValue(object target)
{
return properties;
}
public void SetValue(object target, object value)
{
throw new NotImplementedException("SetValue not implemented for fixed properties; set JsonProperty.Writable = false.");
}
#endregion
}
class NoAttributeProvider : IAttributeProvider
{
#region IAttributeProvider Members
public IList<Attribute> GetAttributes(Type attributeType, bool inherit) { return new Attribute[0]; }
public IList<Attribute> GetAttributes(bool inherit) { return new Attribute[0]; }
#endregion
}
}
Finally, serialize your example type as follows:
var settings = new JsonSerializerSettings
{
ContractResolver = new ConfigurableContractResolver
{
// Here I am using CamelCaseNamingStrategy as is shown in your JSON.
// If you don't want camel case, leave NamingStrategy null.
NamingStrategy = new CamelCaseNamingStrategy(),
}.Configure((s, e) => { e.Contract.AddDateProperties(); }),
};
var json = JsonConvert.SerializeObject(example, Formatting.Indented, settings);
This solution only handles statically typed DateTime and DateTime? properties. If you have object-valued properties that sometimes have DateTime values, or a Dictionary<string, DateTime>, or extension data containing DateTime values, you will need a more complex solution.
(As an alternative implementation, you could instead subclass DefaultContractResolver.CreateObjectContract and hardcode the required properties there using JsonContractExtensions.AddDateProperties(), however I thought it would be more interesting to create a general-purpose, fluently configurable contract resolver in case it becomes necessary to plug in different customizations later.)
You may want to cache the contract resolver for best performance.
Sample .Net fiddle.

id field resolution when using JObject.FromObject

I have a family of custom Json Converters. They work like this:
public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) {
JObject jo = JObject.FromObject(value);
// do my own stuff to the JObject here -- basically adding a property. The value of the property depends on the specific converter being used.
jo.WriteTo(writer, this);
}
The problem with this is that the id field of the JObject is always 1. Not good. So I tried using an inner serializer to get the id field:
private JsonSerializer _InnerSerializer {get;set;}
private JsonSerializer InnerSerializer {
get {
if (_InnerSerializer == null) {
_InnerSerializer = new JsonSerializer();
}
return _InnerSerializer;
}
}
public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) {
JsonSerializer inner = this.InnerSerializer;
jo = JObject.FromObject(value, inner);
//my stuff here
jo.WriteTo(writer, this);
}
That gives a different id each time, even if it hits the same object twice. What I really want is to use Json's usual id resolution with my custom serialization. How can I do that?
Your idea of using an inner serializer will not work as-is. The id-to-object mapping table is held in a private field JsonSerializerInternalBase._mappings with no way to copy it from the outer serializer to the inner serializer.
As an alternative, you could make a recursive call to serialize using the same serializer, and have the converter disable itself using a thread-static pushdown stack along the lines of Generic method of modifying JSON before being returned to client and JSON.Net throws StackOverflowException when using [JsonConvert()]. You would need to enhance the converters in these examples to manually check for and add the necessary "$id" and "$ref" properties by making use of the JsonSerializer.ReferenceResolver property.
However, since your converters just add properties, a more straightforward solution to your problem might be to create a custom contract resolver that allows types to customize their contract as it is generated via a callback method declared in an attribute applied to the type, for instance:
public class ModifierContractResolver : DefaultContractResolver
{
// As of 7.0.1, Json.NET suggests using a static instance for "stateless" contract resolvers, for performance reasons.
// http://www.newtonsoft.com/json/help/html/ContractResolver.htm
// http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_DefaultContractResolver__ctor_1.htm
// "Use the parameterless constructor and cache instances of the contract resolver within your application for optimal performance."
// See also https://stackoverflow.com/questions/33557737/does-json-net-cache-types-serialization-information
static ModifierContractResolver instance;
// Explicit static constructor to tell C# compiler not to mark type as beforefieldinit
static ModifierContractResolver() { instance = new ModifierContractResolver(); }
public static ModifierContractResolver Instance { get { return instance; } }
protected override JsonObjectContract CreateObjectContract(Type objectType)
{
var contract = base.CreateObjectContract(objectType);
// Apply in reverse order so inherited types are applied after base types.
foreach (var attr in objectType.GetCustomAttributes<JsonObjectContractModifierAttribute>(true).Reverse())
{
var modifier = (JsonObjectContractModifier)Activator.CreateInstance(attr.ContractModifierType, true);
modifier.ModifyContract(objectType, contract);
}
return contract;
}
}
public abstract class JsonObjectContractModifier
{
public abstract void ModifyContract(Type objectType, JsonObjectContract contract);
}
[System.AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class JsonObjectContractModifierAttribute : System.Attribute
{
private readonly Type _contractModifierType;
public Type ContractModifierType { get { return _contractModifierType; } }
public JsonObjectContractModifierAttribute(Type contractModifierType)
{
if (contractModifierType == null)
{
throw new ArgumentNullException("contractModifierType");
}
if (!typeof(JsonObjectContractModifier).IsAssignableFrom(contractModifierType))
{
throw new ArgumentNullException(string.Format("{0} is not a subtype of {1}", contractModifierType, typeof(JsonObjectContractModifier)));
}
this._contractModifierType = contractModifierType;
}
}
Then, apply it to your types as in the following example:
[JsonObjectContractModifier(typeof(TestContractModifier))]
public class Test
{
public string A { get; set; }
public string B { get; set; }
public string C { get; set; }
}
class TestContractModifier : JsonObjectContractModifier
{
class EmptyValueProvider : IValueProvider
{
// Explicit static constructor to tell C# compiler not to mark type as beforefieldinit
static EmptyValueProvider() { }
internal static readonly EmptyValueProvider Instance = new EmptyValueProvider();
#region IValueProvider Members
public object GetValue(object target)
{
var test = target as Test;
if (test == null)
return null;
return test.A == null && test.B == null && test.C == null;
}
public void SetValue(object target, object value)
{
var property = target as Test;
if (property == null)
return;
if (value != null && value.GetType() == typeof(bool) && (bool)value == true)
{
property.A = property.B = property.C = null;
}
}
#endregion
}
public override void ModifyContract(Type objectType, JsonObjectContract contract)
{
var jsonProperty = new JsonProperty
{
PropertyName = "isEmpty",
UnderlyingName = "isEmpty",
PropertyType = typeof(bool?),
NullValueHandling = NullValueHandling.Ignore,
Readable = true,
Writable = true,
DeclaringType = typeof(Test),
ValueProvider = EmptyValueProvider.Instance,
};
contract.Properties.Add(jsonProperty);
}
}
And serialize as follows:
var settings = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects, // Or PreserveReferencesHandling.All
ContractResolver = ModifierContractResolver.Instance,
};
var json = JsonConvert.SerializeObject(root, Formatting.Indented, settings);
This produces the following JSON:
[
{
"$id": "1",
"A": "hello",
"B": "goodbye",
"C": "sea",
"isEmpty": false
},
{
"$ref": "1"
},
{
"$id": "2",
"A": null,
"B": null,
"C": null,
"isEmpty": true
},
}
As you can see, both the synthetic "isEmpty" property and reference handling properties are present. Prototype fiddle.

Categories

Resources