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();
Related
I want to omit empty string properties from my Json.
Here's what I did.
Create a Custom Converter that converts empty strings to null:
public class EmptyStringToNullConverter : JsonConverter<string>
{
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.GetString();
}
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
if (value == string.Empty)
writer.WriteNullValue();
else
writer.WriteStringValue(value);
}
}
I have a JsonSerializerClass, which basically is a wrapper around System.Text.Json.JsonSerializer and this class is using the Converter:
public class JsonSerializerClass
{
private JsonSerializerOptions serializeOptions;
private JsonSerializerOptions deserializeOptions;
public JsonSerializerClass()
{
serializeOptions = new JsonSerializerOptions();
deserializeOptions = new JsonSerializerOptions();
InitSerializer();
InitDeserializer();
}
public JsonSerializerOptions SerializeOptions => serializeOptions;
public string Serialize(object value)
{
return Serialize(value, serializeOptions);
}
public T Deserialize<T>(string json)
{
var o = System.Text.Json.JsonSerializer.Deserialize<T>(json, deserializeOptions);
if (o == null)
throw new ArgumentException($"Cannot deserialize JSON to type {typeof(T)}: {json}");
return o;
}
public static string Serialize(object value, JsonSerializerOptions options)
{
return System.Text.Json.JsonSerializer.Serialize(value, options);
}
private void InitSerializer()
{
serializeOptions.Converters.Add(new EmptyStringToNullConverter());
serializeOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
}
private void InitDeserializer()
{
deserializeOptions.PropertyNameCaseInsensitive = true;
deserializeOptions.Converters.Add(new JsonStringEnumConverter());
deserializeOptions.Converters.Add(new DateOnlyConverter());
deserializeOptions.Converters.Add(new DateOnlyNullableConverter());
deserializeOptions.Converters.Add(new IntConverter());
deserializeOptions.Converters.Add(new DecimalConverter());
deserializeOptions.IncludeFields = true;
deserializeOptions.AllowTrailingCommas = true;
deserializeOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString;
deserializeOptions.ReadCommentHandling = JsonCommentHandling.Skip;
}
}
And here's my test case:
var person = new Person
{
Name = "John",
LastName = "Doe",
MiddleName = ""
};
var serializer = new JsonSerializerClass();
var json = serializer.Serialize(person);
json.ToLower().Should().NotContain(nameof(person.MiddleName).ToLower());
I would expect that MiddleName is not in the json because empty string becomes null and null values should be removed from the json. But instead MiddleName is in the json, with value null!
In .NET 7, you can use a Modifier to influence the serialization process (more information in the announcement blog):
static void ExcludeEmptyStrings(JsonTypeInfo jsonTypeInfo)
{
if (jsonTypeInfo.Kind != JsonTypeInfoKind.Object)
return;
foreach (JsonPropertyInfo jsonPropertyInfo in jsonTypeInfo.Properties)
{
if (jsonPropertyInfo.PropertyType == typeof(string))
{
jsonPropertyInfo.ShouldSerialize = static (obj, value) =>
!string.IsNullOrEmpty((string)value);
}
}
}
In this example, it's all about the ShouldSerialize property, which takes in the value and allows you to decide whether or not it should be serialized.
Add the modifier using the property I linked above:
var jsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { ExcludeEmptyStrings }
}
};
.NET Fiddle
you can try this custom converter
var options = new System.Text.Json.JsonSerializerOptions { WriteIndented = true };
options.Converters.Add(new IgnoreNullOrEmptyStringJsonConverter<Person>());
var json = System.Text.Json.JsonSerializer.Serialize(person, options);
public class IgnoreNullOrEmptyStringJsonConverter<T> : System.Text.Json.Serialization.JsonConverter<T> where T : class
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Use default implementation when deserializing (reading)
return System.Text.Json.JsonSerializer.Deserialize<T>(ref reader, options);
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
writer.WriteStartObject();
using (JsonDocument document = System.Text.Json.JsonSerializer.SerializeToDocument(value))
{
foreach (var property in document.RootElement.EnumerateObject())
{
if (property.Value.ValueKind.ToString() != "Null"
&& !(property.Value.ValueKind.ToString() == "String"
&& string.IsNullOrEmpty(property.Value.ToString())))
property.WriteTo(writer);
}
}
writer.WriteEndObject();
}
}
output
{
"Name": "John",
"LastName": "Doe"
}
or you can use this function
var json = GetIgnoreNullOrEmptyStringJson(person);
public string GetIgnoreNullOrEmptyStringJson<T>(T value) where T : class
{
var jNode = System.Text.Json.JsonSerializer.SerializeToNode(value);
var props = jNode.AsObject()
.Where(prop => (prop.Value == null
|| (prop.Value.GetValue<JsonElement>().ValueKind.ToString() == "String"
&& string.IsNullOrEmpty(prop.Value.ToString()))))
.Select(e => e)
.ToArray();
for (var i = 0; i < props.Length; i++)
jNode.AsObject().Remove(props[i].Key);
return System.Text.Json.JsonSerializer.Serialize(jNode, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
}
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.
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;}
}
It is possible to define a custom "list-merge" startegy used for the JsonConvert.PopulateObject method?
Example:
I have two models:
class Parent
{
public Guid Uuid { get; set; }
public string Name { get; set; }
public List<Child> Childs { get; set; }
}
class Child
{
public Guid Uuid { get; set; }
public string Name { get; set; }
public int Score { get; set; }
}
My initial JSON:
{
"Uuid":"cf82b1fd-1ca0-4125-9ea2-43d1d71c9bed",
"Name":"John",
"Childs":[
{
"Uuid":"96b93f95-9ce9-441d-bfb0-f44b65f7fe0d",
"Name":"Philip",
"Score":100
},
{
"Uuid":"fe7837e0-9960-4c45-b5ab-4e4658c08ccd",
"Name":"Peter",
"Score":150
},
{
"Uuid":"1d2cdba4-9efb-44fc-a2f3-6b86a5291954",
"Name":"Steve",
"Score":80
}
]
}
and my update JSON:
{
"Uuid":"cf82b1fd-1ca0-4125-9ea2-43d1d71c9bed",
"Childs":[
{
"Uuid":"fe7837e0-9960-4c45-b5ab-4e4658c08ccd",
"Score":170
}
]
}
All I need is to specify a model property (by attribute) used for matching list items (in my case the Uuid property of Child), so calling the JsonConvert.PopulateObject on the object deserialized from my initial JSON with a update JSON (it contains ONLY changed values + Uuids for every object) results to update only list elements contained in the update JSON macthed by Uuid (in my case update a Peter's score) and elements not contained in the update JSON leave without change.
I'm searching for some universal solution - I need to apply it on large JSONs with a lot of nested lists (but every model has some unique property). So I need to recursively call PopulateObject on matched list item.
You could create your own JsonConverter that implements the required merge logic. This is possible because JsonConverter.ReadJson is passed an existingValue parameter that contains the pre-existing contents of the property being deserialized.
Thus:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class JsonMergeKeyAttribute : System.Attribute
{
}
public class KeyedListMergeConverter : JsonConverter
{
readonly IContractResolver contractResolver;
public KeyedListMergeConverter(IContractResolver contractResolver)
{
if (contractResolver == null)
throw new ArgumentNullException("contractResolver");
this.contractResolver = contractResolver;
}
static bool CanConvert(IContractResolver contractResolver, Type objectType, out Type elementType, out JsonProperty keyProperty)
{
elementType = objectType.GetListType();
if (elementType == null)
{
keyProperty = null;
return false;
}
var contract = contractResolver.ResolveContract(elementType) as JsonObjectContract;
if (contract == null)
{
keyProperty = null;
return false;
}
keyProperty = contract.Properties.Where(p => p.AttributeProvider.GetAttributes(typeof(JsonMergeKeyAttribute), true).Count > 0).SingleOrDefault();
return keyProperty != null;
}
public override bool CanConvert(Type objectType)
{
Type elementType;
JsonProperty keyProperty;
return CanConvert(contractResolver, objectType, out elementType, out keyProperty);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (contractResolver != serializer.ContractResolver)
throw new InvalidOperationException("Inconsistent contract resolvers");
Type elementType;
JsonProperty keyProperty;
if (!CanConvert(contractResolver, objectType, out elementType, out keyProperty))
throw new JsonSerializationException(string.Format("Invalid input type {0}", objectType));
if (reader.TokenType == JsonToken.Null)
return existingValue;
var list = existingValue as IList;
if (list == null || list.Count == 0)
{
list = list ?? (IList)contractResolver.ResolveContract(objectType).DefaultCreator();
serializer.Populate(reader, list);
}
else
{
var jArray = JArray.Load(reader);
var comparer = new KeyedListMergeComparer();
var lookup = jArray.ToLookup(i => i[keyProperty.PropertyName].ToObject(keyProperty.PropertyType, serializer), comparer);
var done = new HashSet<JToken>();
foreach (var item in list)
{
var key = keyProperty.ValueProvider.GetValue(item);
var replacement = lookup[key].Where(v => !done.Contains(v)).FirstOrDefault();
if (replacement != null)
{
using (var subReader = replacement.CreateReader())
serializer.Populate(subReader, item);
done.Add(replacement);
}
}
// Populate the NEW items into the list.
if (done.Count < jArray.Count)
foreach (var item in jArray.Where(i => !done.Contains(i)))
{
list.Add(item.ToObject(elementType, serializer));
}
}
return list;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
class KeyedListMergeComparer : IEqualityComparer<object>
{
#region IEqualityComparer<object> Members
bool IEqualityComparer<object>.Equals(object x, object y)
{
if (object.ReferenceEquals(x, y))
return true;
else if (x == null || y == null)
return false;
return x.Equals(y);
}
int IEqualityComparer<object>.GetHashCode(object obj)
{
if (obj == null)
return 0;
return obj.GetHashCode();
}
#endregion
}
}
public static class TypeExtensions
{
public static Type GetListType(this Type type)
{
while (type != null)
{
if (type.IsGenericType)
{
var genType = type.GetGenericTypeDefinition();
if (genType == typeof(List<>))
return type.GetGenericArguments()[0];
}
type = type.BaseType;
}
return null;
}
}
Notice that the converter needs to know the IContractResolver currently in use. Having it makes finding the key parameter easier, and also ensures that, if the key parameter has a [JsonProperty(name)] attribute, the replacement name is respected.
Then add the attribute:
class Child
{
[JsonMergeKey]
[JsonProperty("Uuid")] // Replacement name for testing
public Guid UUID { get; set; }
public string Name { get; set; }
public int Score { get; set; }
}
And use the converter as follows:
var serializer = JsonSerializer.CreateDefault();
var converter = new KeyedListMergeConverter(serializer.ContractResolver);
serializer.Converters.Add(converter);
using (var reader = new StringReader(updateJson))
{
serializer.Populate(reader, parent);
}
The converter assumes that the key parameter is always present in the JSON. Also, if any entries in the JSON being merged have keys that are not found in the existing list, they are appended to the list.
Update
The original converter is specifically hardcoded for List<T>, and takes advantage of the fact that List<T> implements both IList<T> and IList. If your collection is not a List<T> but still implements IList<T>, the following should work:
public class KeyedIListMergeConverter : JsonConverter
{
readonly IContractResolver contractResolver;
public KeyedIListMergeConverter(IContractResolver contractResolver)
{
if (contractResolver == null)
throw new ArgumentNullException("contractResolver");
this.contractResolver = contractResolver;
}
static bool CanConvert(IContractResolver contractResolver, Type objectType, out Type elementType, out JsonProperty keyProperty)
{
if (objectType.IsArray)
{
// Not implemented for arrays, since they cannot be resized.
elementType = null;
keyProperty = null;
return false;
}
var elementTypes = objectType.GetIListItemTypes().ToList();
if (elementTypes.Count != 1)
{
elementType = null;
keyProperty = null;
return false;
}
elementType = elementTypes[0];
var contract = contractResolver.ResolveContract(elementType) as JsonObjectContract;
if (contract == null)
{
keyProperty = null;
return false;
}
keyProperty = contract.Properties.Where(p => p.AttributeProvider.GetAttributes(typeof(JsonMergeKeyAttribute), true).Count > 0).SingleOrDefault();
return keyProperty != null;
}
public override bool CanConvert(Type objectType)
{
Type elementType;
JsonProperty keyProperty;
return CanConvert(contractResolver, objectType, out elementType, out keyProperty);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (contractResolver != serializer.ContractResolver)
throw new InvalidOperationException("Inconsistent contract resolvers");
Type elementType;
JsonProperty keyProperty;
if (!CanConvert(contractResolver, objectType, out elementType, out keyProperty))
throw new JsonSerializationException(string.Format("Invalid input type {0}", objectType));
if (reader.TokenType == JsonToken.Null)
return existingValue;
var method = GetType().GetMethod("ReadJsonGeneric", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
var genericMethod = method.MakeGenericMethod(new[] { elementType });
try
{
return genericMethod.Invoke(this, new object[] { reader, objectType, existingValue, serializer, keyProperty });
}
catch (TargetInvocationException ex)
{
// Wrap the TargetInvocationException in a JsonSerializationException
throw new JsonSerializationException("ReadJsonGeneric<T> error", ex);
}
}
object ReadJsonGeneric<T>(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer, JsonProperty keyProperty)
{
var list = existingValue as IList<T>;
if (list == null || list.Count == 0)
{
list = list ?? (IList<T>)contractResolver.ResolveContract(objectType).DefaultCreator();
serializer.Populate(reader, list);
}
else
{
var jArray = JArray.Load(reader);
var comparer = new KeyedListMergeComparer();
var lookup = jArray.ToLookup(i => i[keyProperty.PropertyName].ToObject(keyProperty.PropertyType, serializer), comparer);
var done = new HashSet<JToken>();
foreach (var item in list)
{
var key = keyProperty.ValueProvider.GetValue(item);
var replacement = lookup[key].Where(v => !done.Contains(v)).FirstOrDefault();
if (replacement != null)
{
using (var subReader = replacement.CreateReader())
serializer.Populate(subReader, item);
done.Add(replacement);
}
}
// Populate the NEW items into the list.
if (done.Count < jArray.Count)
foreach (var item in jArray.Where(i => !done.Contains(i)))
{
list.Add(item.ToObject<T>(serializer));
}
}
return list;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
class KeyedListMergeComparer : IEqualityComparer<object>
{
#region IEqualityComparer<object> Members
bool IEqualityComparer<object>.Equals(object x, object y)
{
return object.Equals(x, y);
}
int IEqualityComparer<object>.GetHashCode(object obj)
{
if (obj == null)
return 0;
return obj.GetHashCode();
}
#endregion
}
}
public static class TypeExtensions
{
public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
{
if (type == null)
throw new ArgumentNullException();
if (type.IsInterface)
return new[] { type }.Concat(type.GetInterfaces());
else
return type.GetInterfaces();
}
public static IEnumerable<Type> GetIListItemTypes(this Type type)
{
foreach (Type intType in type.GetInterfacesAndSelf())
{
if (intType.IsGenericType
&& intType.GetGenericTypeDefinition() == typeof(IList<>))
{
yield return intType.GetGenericArguments()[0];
}
}
}
}
Note that merging is not implemented for arrays since they are not resizable.
This question already has answers here:
How to handle both a single item and an array for the same property using JSON.net
(9 answers)
Closed 8 years ago.
I have to read a JSON document, which has a field that can contain different types.
For example can be either a long or an array of integers. I know I will need to use a custom deserializer, but am not sure how.
In the example below the xx field sometimes is a long, otherwise an array of ints.
Any help on how to deal with this is appreciated.
static void JsonTest() {
const string json = #"
{
'Code': 'XYZ',
'Response': {
'Type' : 'S',
'Docs': [
{
'id' : 'test1',
'xx' : 1
},
{
'id' : 'test2',
'xx' : [1, 2, 4, 8]
},
]
}
}";
A a;
try {
a = JsonConvert.DeserializeObject<A>(json);
}
catch( Exception ex ) {
Console.Error.WriteLine(ex.Message);
}
}
public class A {
public string Code;
public TResponse Response;
}
public class TResponse {
public string Type;
public List<Doc> Docs;
}
public class Doc {
public string id;
public int[] xx;
}
My implementation based on the suggestion below (changed array to long from int):
[JsonConverter(typeof(DocConverter))]
public class Doc {
public string id;
public long[] xx;
}
public class DocConverter : JsonConverter {
public override bool CanWrite { get { return false; } }
public override bool CanConvert( Type objectType ) {
return typeof(Doc).IsAssignableFrom(objectType);
}
public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer ) {
JObject item = JObject.Load(reader);
Doc doc = new Doc();
doc.id = item["id"].ToObject<string>();
if( item["xx"].Type == JTokenType.Long )
doc.xx = new [] { item["xx"].ToObject<long>() };
else
doc.xx = item["xx"].ToObject<long[]>();
return doc;
}
public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer ) {
throw new NotImplementedException();
}
}
Since xx can either be a long or an array of ints, it makes sense to turn Doc into a class hierarchy. (If it were a single long or an array of longs, it would make sense to read them all into a single class.)
You can do this by using a JsonConverter, like so:
[JsonConverter(typeof(DocConverter))]
public abstract class Doc
{
public string id;
}
[JsonConverter(typeof(NoConverter))] // Prevents infinite recursion when converting a class instance known to be of type DocSingle
public class DocSingle : Doc
{
public long xx;
}
[JsonConverter(typeof(NoConverter))] // Prevents infinite recursion when converting a class instance known to be of type DocList
public class DocList : Doc
{
public int[] xx;
}
public class DocConverter : JsonConverter
{
public override bool CanWrite { get { return false; } }
public override bool CanConvert(Type objectType)
{
return typeof(Doc).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader,
Type objectType, object existingValue, JsonSerializer serializer)
{
JObject item = JObject.Load(reader);
if (item["xx"].Type == JTokenType.Integer)
{
return item.ToObject<DocSingle>();
}
else
{
return item.ToObject<DocList>();
}
}
public override void WriteJson(JsonWriter writer,
object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public class NoConverter : JsonConverter
{
public override bool CanRead { get { return false; } }
public override bool CanWrite { get { return false; } }
public override bool CanConvert(Type objectType)
{
return false;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Update
incidentally, if you're willing to simplify your data model to say that xx can either be a single long or an array of longs, you can simplify the code as follows:
[JsonConverter(typeof(DocConverter))]
public sealed class Doc
{
public string id;
public long[] xx;
}
public class DocConverter : JsonConverter
{
public override bool CanWrite { get { return true; } }
public override bool CanConvert(Type objectType)
{
return typeof(Doc).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader,
Type objectType, object existingValue, JsonSerializer serializer)
{
JObject item = JObject.Load(reader);
var doc = new Doc();
JToken id = item["id"];
if (id != null)
doc.id = id.ToString();
JToken xx = item["xx"];
if (xx != null)
{
if (xx.Type == JTokenType.Integer)
{
var val = (long)xx;
doc.xx = new long[] { val };
}
else if (xx.Type == JTokenType.Array)
{
var val = xx.ToObject<long[]>();
doc.xx = val;
}
else
{
Debug.WriteLine("Unknown type of JToken for \"xx\": " + xx.ToString());
}
}
return doc;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var doc = (Doc)value;
writer.WriteStartObject();
writer.WritePropertyName("id");
writer.WriteValue(doc.id);
var xx = doc.xx;
if (xx != null)
{
writer.WritePropertyName("xx");
if (xx.Length == 1)
{
writer.WriteValue(xx[0]);
}
else
{
writer.WriteStartArray();
foreach (var x in xx)
{
writer.WriteValue(x);
}
writer.WriteEndArray();
}
}
writer.WriteEndObject();
}
}
You have a string, try json.Contains("'Type':'S'").
Then deserialize it to the proper model.