0

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);
 }
}
asked Jul 28, 2017 at 7:24
5
  • As mentioned in this answer and this one, Json.NET supports the XXXSpecified pattern. Maybe that meets your needs? Commented Jul 28, 2017 at 7:32
  • @dbc That's much simpler approach for serialisation, but I'd loose the ability to distinguish between an empty key and null during deserialization. Commented Jul 28, 2017 at 7:47
  • 1
    The xxxSpecified property should get set if and only if a property with the name xxx is encountered - even with a null value. Isn't that what you want? Commented Jul 28, 2017 at 7:57
  • That'd be perfect then, thanks! I found using a [JsonIgnore] attribute was simpler than implementing a custom ContractResolver for omiting the xxxSpecified property. Commented Jul 28, 2017 at 8:10
  • Here's another approach that you may find interesting: stackoverflow.com/questions/44928074/… Commented Jul 30, 2017 at 9:57

1 Answer 1

2

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;}
}
answered Jul 28, 2017 at 8:14
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.