Deserializing Heterogenous JSON Array into Covariant List<> Using JSON.Net

Deserializing heterogenous JSON array into covariant List using Json.NET

Here is an example using CustomCreationConverter.

public class JsonItemConverter :  Newtonsoft.Json.Converters.CustomCreationConverter<Item>
{
public override Item Create(Type objectType)
{
throw new NotImplementedException();
}

public Item Create(Type objectType, JObject jObject)
{
var type = (string)jObject.Property("valueType");
switch (type)
{
case "int":
return new IntItem();
case "string":
return new StringItem();
}

throw new ApplicationException(String.Format("The given vehicle type {0} is not supported!", type));
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Load JObject from stream
JObject jObject = JObject.Load(reader);

// Create target object based on JObject
var target = Create(objectType, jObject);

// Populate the object properties
serializer.Populate(jObject.CreateReader(), target);

return target;
}
}

public abstract class Item
{
public string ValueType { get; set; }

[JsonProperty("valueTypeId")]
public int ValueTypeId { get; set; }

[JsonProperty("name")]
public string Name { get; set; }

public new virtual string ToString() { return "Base object, we dont' want base created ValueType=" + this.ValueType + "; " + "name: " + Name; }
}

public class StringItem : Item
{
[JsonProperty("value")]
public string Value { get; set; }

[JsonProperty("numberChars")]
public int NumberCharacters { get; set; }

public override string ToString() { return "StringItem object ValueType=" + this.ValueType + ", Value=" + this.Value + "; " + "Num Chars= " + NumberCharacters; }

}

public class IntItem : Item
{
[JsonProperty("value")]
public int Value { get; set; }

public override string ToString() { return "IntItem object ValueType=" + this.ValueType + ", Value=" + this.Value; }
}

class Program
{
static void Main(string[] args)
{
// json string
var json = "[{\"value\":5,\"valueType\":\"int\",\"valueTypeId\":1,\"name\":\"numberOfDups\"},{\"value\":\"some thing\",\"valueType\":\"string\",\"valueTypeId\":1,\"name\":\"a\",\"numberChars\":11},{\"value\":2,\"valueType\":\"int\",\"valueTypeId\":2,\"name\":\"b\"}]";

// The above is deserialized into a list of Items, instead of a hetrogenous list of
// IntItem and StringItem
var result = JsonConvert.DeserializeObject<List<Item>>(json, new JsonItemConverter());

foreach (var r in result)
{
// r is an instance of Item not StringItem or IntItem
Console.WriteLine("got " + r.ToString());
}
}
}

How to use JsonSerializer to deserialize a heterogenous JSON Array?

I came across this test in the System.Text.Json source code which uses a custom JsonConverter to mimic Newtonsoft.Json behavior for object deserialization (which does maintain the original object type instead).

Here's a slimmed down version of that custom converter that could be used to achieve this:

class SystemObjectNewtonsoftCompatibleConverter : JsonConverter<object>
{
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.True => true,
JsonTokenType.False => false,
JsonTokenType.Number => reader.GetInt32(),
JsonTokenType.String => reader.GetString(),
_ => Fallback(ref reader)
};

object Fallback(ref Utf8JsonReader reader)
{
// Use JsonElement as fallback.
// Newtonsoft uses JArray or JObject.
using JsonDocument document = JsonDocument.ParseValue(ref reader);
return document.RootElement.Clone();
}
}

public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) =>
throw new InvalidOperationException("Should not get here.");
}

This example using that converter shows the object types being preserved:

const string json = @"{
""MethodName"": ""LaunchRockets"",
""MethodParameters"": [ ""Long Range"", 100, true ]
}";

var options = new JsonSerializerOptions();
options.Converters.Add(new SystemObjectNewtonsoftCompatibleConverter());
MethodCallDescription instance = JsonSerializer.Deserialize<MethodCallDescription>(json, options);
Console.WriteLine(instance.MethodName);
Console.WriteLine(string.Join(", ", instance.MethodParameters.Select(p => (p, p.GetType().Name))));

Giving the ouput:

LaunchRockets
(Long Range, String), (100, Int32), (True, Boolean)

Deserialize class with an array of base objects into class with an array of inherited objects (Newtonsoft.Json)

I try with this code:

var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All
};

var chart = new Chart();
chart.note = new Note[]
{
new NoteSingle { x = 37 },
new NoteRain { endbeat = new[] { 9 } }
};

var json = JsonConvert.SerializeObject(chart, settings);
var chart2 = JsonConvert.DeserializeObject<Chart>(json, settings);

And it's working. json has this value:

{
"$type":"Test.Chart, SoApp",
"note":{
"$type":"Test.Chart+Note[], SoApp",
"$values":[
{
"$type":"Test.Chart+NoteSingle, SoApp",
"x":37,
"beat":null
},
{
"$type":"Test.Chart+NoteRain, SoApp",
"endbeat":{
"$type":"System.Int32[], mscorlib",
"$values":[
9
]
},
"beat":null
}
]
}
}

And chart2 has 2 notes of NoteSingle and NoteRain types. Maybe you aren't using TypeNameHandling.All in Serialize. You need to use both on Serialize and Deserialize.

UPDATE

If you haven't control of the generated JSON, you can use a Converter to deserialize it:

public class YourJsonConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var chart = new Chart();
var notes = new List<Note>();

string name = null;
NoteRain noteRain = null;

while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
name = reader.Value.ToString();
break;

case JsonToken.Integer:
if (name == "x")
{
var note = new NoteSingle { x = Convert.ToInt32(reader.Value) };
notes.Add(note);
}
else if (name == "endbeat")
{
if (noteRain == null)
{
noteRain = new NoteRain { endbeat = new[] { Convert.ToInt32(reader.Value) } };
notes.Add(noteRain);
}
else
{
var array = noteRain.endbeat;
noteRain.endbeat = new int[noteRain.endbeat.Length + 1];

for (int i = 0; i < array.Length; i++)
{
noteRain.endbeat[i] = array[i];
}

noteRain.endbeat[noteRain.endbeat.Length - 1] = Convert.ToInt32(reader.Value);
}
}

break;
}
}

chart.note = notes.ToArray();
return chart;
}

public override bool CanWrite => false;

public override bool CanConvert(Type objectType)
{
return true;
}
}

This is a simple example, you must tune it but I think it's easy to do. In the property name I get the name of the property and I use later to create the correct type. If I process a x property I know that is a NoteSingle and I create it and add to notes list.
If, for example, you get a property name like beat and you don't know yet the type of the Note class, use a temporary variable to store and fill and later, when you read a property that you know is from a concrete class, create the Note instance and fill with this variable. And after that, If you read more data of this class, continue filling your instance.

Usage:

var settings = new JsonSerializerSettings();
settings.Converters.Add(new YourJsonConverter());
var chart = JsonConvert.DeserializeObject<Chart>(json, settings);

Newtonsoft Json.Net - How to conditionally add (or skip) items in an array when deserializing?

You can create the following JsonConverter<T []> that will skip array entries beyond a certain count:

public class MaxLengthArrayConverter<T> : JsonConverter<T []>
{
public MaxLengthArrayConverter(int maxLength) => this.MaxLength = maxLength >= 0 ? maxLength : throw new ArgumentException(nameof(maxLength));

public int MaxLength { get; }

public override T [] ReadJson(JsonReader reader, Type objectType, T [] existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
return null;
reader.AssertTokenType(JsonToken.StartArray);
var list = new List<T>();
while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndArray)
{
if (list.Count < MaxLength)
list.Add(serializer.Deserialize<T>(reader));
else
reader.Skip();
}
return list.ToArray();
}

public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, T [] value, JsonSerializer serializer) => throw new NotImplementedException();
}

public static partial class JsonExtensions
{
public static JsonReader AssertTokenType(this JsonReader reader, JsonToken tokenType) =>
reader.TokenType == tokenType ? reader : throw new JsonSerializationException(string.Format("Unexpected token {0}, expected {1}", reader.TokenType, tokenType));

public static JsonReader ReadToContentAndAssert(this JsonReader reader) =>
reader.ReadAndAssert().MoveToContentAndAssert();

public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}

public static JsonReader ReadAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
}

Then, assuming your financial market orderbook model looks something like this:

public class OrderBook
{
public Order [] Orders { get; set; }
}

You can deserialize as follows:

int maxLength = 20;  // Or whatever you want.
var settings = new JsonSerializerSettings
{
Converters = { new MaxLengthArrayConverter<Order>(maxLength) },
};
var model = JsonConvert.DeserializeObject<OrderBook>(json, settings);

Assert.IsTrue(model.Orders.Length <= maxLength);

Notes:

  • In your question you mention only arrays, but if your model is actually using lists rather than arrays, use the following converter instead:

    public class MaxLengthListConverter<T> : JsonConverter<List<T>>
    {
    public MaxLengthListConverter(int maxLength) => this.MaxLength = maxLength >= 0 ? maxLength : throw new ArgumentException(nameof(maxLength));

    public int MaxLength { get; }

    public override List<T> ReadJson(JsonReader reader, Type objectType, List<T> existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
    if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
    return null;
    reader.AssertTokenType(JsonToken.StartArray);
    existingValue ??= (List<T>)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
    existingValue.Clear();
    while (reader.ReadToContentAndAssert().TokenType != JsonToken.EndArray)
    {
    if (existingValue.Count < MaxLength)
    existingValue.Add(serializer.Deserialize<T>(reader));
    else
    reader.Skip();
    }
    return existingValue;
    }

    public override bool CanWrite => false;
    public override void WriteJson(JsonWriter writer, List<T> value, JsonSerializer serializer) => throw new NotImplementedException();
    }
  • This answer assumes that you want all arrays of type T [] in your model to be truncated to some specific length in runtime. If this is not true, and you need different max lengths for different arrays to be individually specified in runtime, you will need a more complex solution, probably involving a custom contract resolver.

Demo fiddle here.

Json conversion conundrum due to mixed type in List

Try this, it was tested in Visual studio

    var jsonObject = JObject.Parse(json);

var pars = jsonObject["params"] as JArray;

var paramElement = new ParamElement();

foreach (var jObject in pars)
{
if (jObject.GetType().Name.ToString() == "JValue") paramElement.ParamName = ((JValue)jObject).ToString();
else
{
paramElement.ParamBody=jObject.ToObject<ParamClass>();
}
}

GateIoTicker gateIoTicker = new GateIoTicker {Params= new List<ParamElement>()};
gateIoTicker.Id= (string) jsonObject["Id"];
gateIoTicker.Method= (string) jsonObject["method"];
gateIoTicker.Params.Add(paramElement);

ParamElement class

public partial class ParamElement
{
public string ParamName { get; set; }
public ParamClass ParamBody {get; set;}

}

How to control deserialization of large array of heterogenous objects in JSON.net

Putting together answers to

  • Deserialize json array stream one item at a time
  • Deserializing polymorphic json classes without type information using json.net,

First, assume you have a custom SerializationBinder (or something similar) that will map type names to types.

Next, you can enumerate through the top-level objects in streaming JSON data (walking into top-level arrays) with the following extension method:

public static class JsonExtensions
{
public static IEnumerable<JObject> WalkObjects(TextReader textReader)
{
using (JsonTextReader reader = new JsonTextReader(textReader))
{
while (reader.Read())
{
if (reader.TokenType == JsonToken.StartObject)
{
JObject obj = JObject.Load(reader);
if (obj != null)
{
yield return obj;
}
}
}
}
}
}

Then, assuming you have some stream for reading your JSON data, you can stream the JSON in and convert top-level array elements one by one for processing as follows:

        SerializationBinder binder = new MyBinder(); // Your custom binder.
using (var stream = GetStream(json))
using (var reader = new StreamReader(stream, Encoding.Unicode))
{
var assemblyName = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;
var items = from obj in JsonExtensions.WalkObjects(reader)
let jType = obj["Type"]
let jInstance = obj["Instance"]
where jType != null && jType.Type == JTokenType.String
where jInstance != null && jInstance.Type == JTokenType.Object
let type = binder.BindToType(assemblyName, (string)jType)
where type != null
select jInstance.ToObject(type); // Deserialize to bound type!

foreach (var item in items)
{
// Handle each item.
Debug.WriteLine(JsonConvert.SerializeObject(item));
}
}

I want JSON to skip deserializing JSON object into C# Listobject if JSON member is missing

You could adopt and extend the general approach from this answer to Deserializing polymorphic json classes without type information using json.net or this answer to How to call JsonConvert.DeserializeObject and disable a JsonConverter applied to a base type via [JsonConverter]? by creating a custom JsonConverter for List<IMachineInfo> that does the following:

  1. Loads each array entry into a temporary JToken.
  2. Tries to infer the item type from the parameters present.
  3. Skips the array entry if the type cannot be inferred.

To do this, first introduce a generic base class converter as follows:

public abstract class JsonListItemTypeInferringConverterBase<TItem> : JsonConverter
{
public override bool CanWrite { get { return false; } }

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}

protected abstract bool TryInferItemType(Type objectType, JToken json, out Type type);

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Get contract information
var contract = serializer.ContractResolver.ResolveContract(objectType) as JsonArrayContract;
if (contract == null || contract.IsMultidimensionalArray || objectType.IsArray)
throw new JsonSerializationException(string.Format("Invalid array contract for {0}", objectType));

if (reader.MoveToContent().TokenType == JsonToken.Null)
return null;

if (reader.TokenType != JsonToken.StartArray)
throw new JsonSerializationException(string.Format("Expected {0}, encountered {1} at path {2}", JsonToken.StartArray, reader.TokenType, reader.Path));

var collection = existingValue as IList<TItem> ?? (IList<TItem>)contract.DefaultCreator();

// Process the collection items
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.EndArray:
return collection;

case JsonToken.Comment:
break;

default:
{
var token = JToken.Load(reader);
Type itemType;
if (!TryInferItemType(typeof(TItem), token, out itemType))
break;
collection.Add((TItem)serializer.Deserialize(token.CreateReader(), itemType));
}
break;
}
}
// Should not come here.
throw new JsonSerializationException("Unclosed array at path: " + reader.Path);
}

public override bool CanConvert(Type objectType)
{
return objectType.IsAssignableFrom(typeof(List<TItem>));
}
}

public static partial class JsonExtensions
{
public static JsonReader MoveToContent(this JsonReader reader)
{
while ((reader.TokenType == JsonToken.Comment || reader.TokenType == JsonToken.None) && reader.Read())
;
return reader;
}
}

Then, make a concrete version that only deserializes list entries of type Machine1 as follows:

public class Machine1ListConverter<TMachineInfo> : JsonListItemTypeInferringConverterBase<TMachineInfo> where TMachineInfo : IMachineInfo
{
protected override bool TryInferItemType(Type objectType, JToken json, out Type type)
{
var obj = json as JObject;
if (obj != null && obj.GetValue("powerWatts", StringComparison.OrdinalIgnoreCase) != null)
{
type = typeof(Machine1);
return true;
}
type = null;
return false;
}
}

And finally deserialize your JSON string as follows:

var settings = new JsonSerializerSettings
{
Converters = { new Machine1ListConverter<IMachineInfo>() },
};
var list = JsonConvert.DeserializeObject<List<IMachineInfo>>(jsonString, settings);

Of if you want to deserialize to a concrete List<Machine1> do:

var settings = new JsonSerializerSettings
{
Converters = { new Machine1ListConverter<Machine1>() },
};
var list = JsonConvert.DeserializeObject<List<Machine1>>(jsonString, settings);

Notes:

  • The converter needs to be applied to the overall collection instead of the collection items because JsonConverter.ReadJson has no ability to skip the token currently being read and prevent its return value from being added to the containing object.

  • To deserialize only the items of type Machine2 you could similarly create Machine2ListConverter in which TryInferItemType() checks for the presence of dbm.

  • To deserialize you are calling

    JsonConvert.DeserializeObject<List<Machine1>>(@"C/Path/jsonfile", settings);

    But JsonConvert.DeserializeObject Method (String, JsonSerializerSettings) deserializes a JSON string, not a named file. To deserialize from a file see Deserialize JSON from a file.

Demo fiddle here.

Is polymorphic deserialization possible in System.Text.Json?

Is polymorphic deserialization possible in System.Text.Json?

The answer is yes and no, depending on what you mean by "possible".

There is no polymorphic deserialization (equivalent to Newtonsoft.Json's TypeNameHandling) support built-in to System.Text.Json. This is because reading the .NET type name specified as a string within the JSON payload (such as $type metadata property) to create your objects is not recommended since it introduces potential security concerns (see https://github.com/dotnet/corefx/issues/41347#issuecomment-535779492 for more info).

Allowing the payload to specify its own type information is a common source of vulnerabilities in web applications.

However, there is a way to add your own support for polymorphic deserialization by creating a JsonConverter<T>, so in that sense, it is possible.

The docs show an example of how to do that using a type discriminator property:
https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to#support-polymorphic-deserialization

Let's look at an example.

Say you have a base class and a couple of derived classes:

public class BaseClass
{
public int Int { get; set; }
}
public class DerivedA : BaseClass
{
public string Str { get; set; }
}
public class DerivedB : BaseClass
{
public bool Bool { get; set; }
}

You can create the following JsonConverter<BaseClass> that writes the type discriminator while serializing and reads it to figure out which type to deserialize. You can register that converter on the JsonSerializerOptions.

public class BaseClassConverter : JsonConverter<BaseClass>
{
private enum TypeDiscriminator
{
BaseClass = 0,
DerivedA = 1,
DerivedB = 2
}

public override bool CanConvert(Type type)
{
return typeof(BaseClass).IsAssignableFrom(type);
}

public override BaseClass Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}

if (!reader.Read()
|| reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "TypeDiscriminator")
{
throw new JsonException();
}

if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}

BaseClass baseClass;
TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
switch (typeDiscriminator)
{
case TypeDiscriminator.DerivedA:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));
break;
case TypeDiscriminator.DerivedB:
if (!reader.Read() || reader.GetString() != "TypeValue")
{
throw new JsonException();
}
if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));
break;
default:
throw new NotSupportedException();
}

if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
{
throw new JsonException();
}

return baseClass;
}

public override void Write(
Utf8JsonWriter writer,
BaseClass value,
JsonSerializerOptions options)
{
writer.WriteStartObject();

if (value is DerivedA derivedA)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedA);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedA);
}
else if (value is DerivedB derivedB)
{
writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);
writer.WritePropertyName("TypeValue");
JsonSerializer.Serialize(writer, derivedB);
}
else
{
throw new NotSupportedException();
}

writer.WriteEndObject();
}
}

This is what serialization and deserialization would look like (including comparison with Newtonsoft.Json):

private static void PolymorphicSupportComparison()
{
var objects = new List<BaseClass> { new DerivedA(), new DerivedB() };

// Using: System.Text.Json
var options = new JsonSerializerOptions
{
Converters = { new BaseClassConverter() },
WriteIndented = true
};

string jsonString = JsonSerializer.Serialize(objects, options);
Console.WriteLine(jsonString);
/*
[
{
"TypeDiscriminator": 1,
"TypeValue": {
"Str": null,
"Int": 0
}
},
{
"TypeDiscriminator": 2,
"TypeValue": {
"Bool": false,
"Int": 0
}
}
]
*/

var roundTrip = JsonSerializer.Deserialize<List<BaseClass>>(jsonString, options);

// Using: Newtonsoft.Json
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,
Formatting = Newtonsoft.Json.Formatting.Indented
};

jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(objects, settings);
Console.WriteLine(jsonString);
/*
[
{
"$type": "PolymorphicSerialization.DerivedA, PolymorphicSerialization",
"Str": null,
"Int": 0
},
{
"$type": "PolymorphicSerialization.DerivedB, PolymorphicSerialization",
"Bool": false,
"Int": 0
}
]
*/

var originalList = JsonConvert.DeserializeObject<List<BaseClass>>(jsonString, settings);

Debug.Assert(originalList[0].GetType() == roundTrip[0].GetType());
}


Related Topics



Leave a reply



Submit