I have a asp.net core method, I want it to accept RAW json, I do not and will not always know the schema of the json object, so I am using dynamic types with dot notation.
This method works when I string the json escaping each character. I have tried to use the json body directly, but this did not work. So it seems my option were to Serialize and then Deserialize the json. ( very redundant) but it seems to throw error any other way if I try to use the JSON body directly.
In the debugger, everything seems to work with the Serialize and Deserialize of the object / string, but throws an error on the id(property) when I try to cast the object to string and gives the error. (In the debugger I am able to see the Id correctly though).
({Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: Cannot convert type 'System.Text.Json.JsonElement' to 'string')}
I really do not see why it gives the type as a string yet cannot convert it. I have even tried to remove the casting, and still receive this error.
public IActionResult Post([FromBody] ExpandoObject requestInput)
{
try
{
//Makes a JSON String
var stringObject = (string) JsonSerializer.Serialize(requestInput);
DateTime time = DateTime.UtcNow;
// Recreated the Json Object
dynamic requestObject = JsonSerializer.Deserialize<ExpandoObject>(stringObject);
// Throws Error here, yet it shows Value as the correct Id number (Value: Type String)
string reqObject = (string) requestObject.Id;
So there is no support for ExpandoObject in .NET Core, yet. MS says that maybe it will be added in .NET 5.0. Until then, you can use this JsonConverter I found on a thread. I will post the code here in case that thread goes away.
You can use it like this:
[HttpPost, Route("testPost")]
public IActionResult TestPost([FromBody] object obj) // just use "object"
{
// object is: { "hello":"world" }
var myDynamic = JsonSerializer.Deserialize<dynamic>(
JsonSerializer.Serialize(obj), new JsonSerializerOptions
{
Converters = { new DynamicJsonConverter() }
});
var test = (string)myDynamic.hello;
// test will equal "world"
return Ok();
}
Here is the converter:
/// <summary>
/// Temp Dynamic Converter
/// by:tchivs#live.cn
/// </summary>
public class DynamicJsonConverter : JsonConverter<dynamic>
{
public override dynamic Read(ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.True)
{
return true;
}
if (reader.TokenType == JsonTokenType.False)
{
return false;
}
if (reader.TokenType == JsonTokenType.Number)
{
if (reader.TryGetInt64(out long l))
{
return l;
}
return reader.GetDouble();
}
if (reader.TokenType == JsonTokenType.String)
{
if (reader.TryGetDateTime(out DateTime datetime))
{
return datetime;
}
return reader.GetString();
}
if (reader.TokenType == JsonTokenType.StartObject)
{
using JsonDocument documentV = JsonDocument.ParseValue(ref reader);
return ReadObject(documentV.RootElement);
}
// Use JsonElement as fallback.
// Newtonsoft uses JArray or JObject.
JsonDocument document = JsonDocument.ParseValue(ref reader);
return document.RootElement.Clone();
}
private object ReadObject(JsonElement jsonElement)
{
IDictionary<string, object> expandoObject = new ExpandoObject();
foreach (var obj in jsonElement.EnumerateObject())
{
var k = obj.Name;
var value = ReadValue(obj.Value);
expandoObject[k] = value;
}
return expandoObject;
}
private object? ReadValue(JsonElement jsonElement)
{
object? result = null;
switch (jsonElement.ValueKind)
{
case JsonValueKind.Object:
result = ReadObject(jsonElement);
break;
case JsonValueKind.Array:
result = ReadList(jsonElement);
break;
case JsonValueKind.String:
//TODO: Missing Datetime&Bytes Convert
result = jsonElement.GetString();
break;
case JsonValueKind.Number:
//TODO: more num type
result = 0;
if (jsonElement.TryGetInt64(out long l))
{
result = l;
}
break;
case JsonValueKind.True:
result = true;
break;
case JsonValueKind.False:
result = false;
break;
case JsonValueKind.Undefined:
case JsonValueKind.Null:
result = null;
break;
default:
throw new ArgumentOutOfRangeException();
}
return result;
}
private object? ReadList(JsonElement jsonElement)
{
IList<object?> list = new List<object?>();
foreach (var item in jsonElement.EnumerateArray())
{
list.Add(ReadValue(item));
}
return list.Count == 0 ? null : list;
}
public override void Write(Utf8JsonWriter writer,
object value,
JsonSerializerOptions options)
{
// writer.WriteStringValue(value.ToString());
}
}
Edited To Add:
Here is a much slicker way to handle dynamic using the converter above as pointed out by Aluan in the comments. In your Startup.cs class, add this:
services.AddControllers().AddJsonOptions(options =>
options.JsonSerializerOptions.Converters.Add(new DynamicJsonConverter()));
Then you don't have to do any goofy stuff in your controller. You can just set the body parameter as dynamic and it magically works:
[HttpPost, Route("testPost")]
public IActionResult TestPost([FromBody] dynamic obj)
{
// object is: { "hello":"world" }
var test = (string)obj.hello;
// test will equal "world"
return Ok();
}
Way nicer!
Related
I'm using .net core 3.1 and library System.Text.Json
How can I deserialize nested json object to Dictionary<string, object>, but the expectation is that based on json property type I'll get proper C# type:
String -> string
Number -> int/double
Object -> Dictionary<string, object>
By default - if I try to deserialize to Dictionary<string, object> - basically every object is a JsonElement.
I'd like it to be of type as mentioned above.
Any idea how it can be achieved?
In order to deserialize free-form JSON into .Net primitive types instead of JsonElement objects, you will need to write a custom JsonConverter, as no such functionality is provided by System.Text.Json out of the box.
One such converter is the following:
public class ObjectAsPrimitiveConverter : JsonConverter<object>
{
FloatFormat FloatFormat { get; init; }
UnknownNumberFormat UnknownNumberFormat { get; init; }
ObjectFormat ObjectFormat { get; init; }
public ObjectAsPrimitiveConverter() : this(FloatFormat.Double, UnknownNumberFormat.Error, ObjectFormat.Expando) { }
public ObjectAsPrimitiveConverter(FloatFormat floatFormat, UnknownNumberFormat unknownNumberFormat, ObjectFormat objectFormat)
{
this.FloatFormat = floatFormat;
this.UnknownNumberFormat = unknownNumberFormat;
this.ObjectFormat = objectFormat;
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
if (value.GetType() == typeof(object))
{
writer.WriteStartObject();
writer.WriteEndObject();
}
else
{
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
}
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
switch (reader.TokenType)
{
case JsonTokenType.Null:
return null;
case JsonTokenType.False:
return false;
case JsonTokenType.True:
return true;
case JsonTokenType.String:
return reader.GetString();
case JsonTokenType.Number:
{
if (reader.TryGetInt32(out var i))
return i;
if (reader.TryGetInt64(out var l))
return l;
// BigInteger could be added here.
if (FloatFormat == FloatFormat.Decimal && reader.TryGetDecimal(out var m))
return m;
else if (FloatFormat == FloatFormat.Double && reader.TryGetDouble(out var d))
return d;
using var doc = JsonDocument.ParseValue(ref reader);
if (UnknownNumberFormat == UnknownNumberFormat.JsonElement)
return doc.RootElement.Clone();
throw new JsonException(string.Format("Cannot parse number {0}", doc.RootElement.ToString()));
}
case JsonTokenType.StartArray:
{
var list = new List<object>();
while (reader.Read())
{
switch (reader.TokenType)
{
default:
list.Add(Read(ref reader, typeof(object), options));
break;
case JsonTokenType.EndArray:
return list;
}
}
throw new JsonException();
}
case JsonTokenType.StartObject:
var dict = CreateDictionary();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.EndObject:
return dict;
case JsonTokenType.PropertyName:
var key = reader.GetString();
reader.Read();
dict.Add(key, Read(ref reader, typeof(object), options));
break;
default:
throw new JsonException();
}
}
throw new JsonException();
default:
throw new JsonException(string.Format("Unknown token {0}", reader.TokenType));
}
}
protected virtual IDictionary<string, object> CreateDictionary() =>
ObjectFormat == ObjectFormat.Expando ? new ExpandoObject() : new Dictionary<string, object>();
}
public enum FloatFormat
{
Double,
Decimal,
}
public enum UnknownNumberFormat
{
Error,
JsonElement,
}
public enum ObjectFormat
{
Expando,
Dictionary,
}
And to use it, deserialize to object (or dynamic if configured to use ExpandoObject) as follows:
var options = new JsonSerializerOptions
{
Converters = { new ObjectAsPrimitiveConverter(floatFormat : FloatFormat.Double, unknownNumberFormat : UnknownNumberFormat.Error, objectFormat : ObjectFormat.Expando) },
WriteIndented = true,
};
dynamic d = JsonSerializer.Deserialize<dynamic>(json, options);
Notes:
JSON allows for numbers of arbitrary precision and magnitude, while the .Net primitive numeric types do not. In situations where some JSON number cannot be parsed into a .Net primitive type, the converter provides the option to either return a JsonElement for the number, or throw an exception.
The converter could be extended to attempt to deserialize unsupported numbers to BigInteger.
You can configure the converter to use double or decimal for floating point numbers, and Dictionary<string, object> or ExpandoObject for JSON objects.
Demo fiddle here.
I have this json
{"id":"48e86841-f62c-42c9-ae20-b54ba8c35d6d"}
How do I get the 48e86841-f62c-42c9-ae20-b54ba8c35d6d out of it? All examples I can find show to do something like
var o = System.Text.Json.JsonSerializer.Deserialize<some-type>(json);
o.id // <- here's the ID!
But I don't have a type that fits this definition and I don't want to create one. I've tried deserializing to dynamic but I was unable to get that working.
var result = System.Text.Json.JsonSerializer.Deserialize<dynamic>(json);
result.id // <-- An exception of type 'Microsoft.CSharp.RuntimeBinder.RuntimeBinderException' occurred in System.Linq.Expressions.dll but was not handled in user code: ''System.Text.Json.JsonElement' does not contain a definition for 'id''
Can anyone give any suggestions?
edit:
I just figured out I can do this:
Guid id = System.Text.Json.JsonDocument.Parse(json).RootElement.GetProperty("id").GetGuid();
This does work - but is there a better way?
you can deserialize to a Dictionary:
var dict = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(json)
Or just deserialize to Object which will yield a JsonElement that you can call GetProperty on.
Support for JsonObject has been added in .NET 6 using System.Text.Json.Nodes.
Example:
const string Json = "{\"MyNumber\":42, \"MyArray\":[10,11]}";
// dynamic
{
dynamic obj = JsonNode.Parse(Json);
int number = (int)obj["MyNumber"];
Debug.Assert(number == 42);
obj["MyString"] = "Hello";
Debug.Assert((string)obj["MyString"] == "Hello");
}
// JsonObject
{
JsonObject obj = JsonNode.Parse(Json).AsObject();
int number = (int)obj["MyNumber"];
Debug.Assert(number == 42);
obj["MyString"] = "Hello";
Debug.Assert((string)obj["MyString"] == "Hello");
}
Sources:
https://github.com/dotnet/runtime/issues/53195
https://github.com/dotnet/runtime/issues/45188
I've recently migrated a project from ASP.NET Core 2.2 to 3, and I'm having this inconvenience. In our team we value lean dependencies, so we are trying to avoid including Newtonsoft.JSON back and try using System.Text.Json. We also decided not to use a ton of POCO objects just for JSON serialization, because our backend models are more complex than needed for Web APIs. Also, because of nontrivial behaviour encapsulation, the backend models cannot be easily used to serialize/deserialize JSON strings.
I understand that System.Text.Json is supposed to be faster than Newtonsoft.JSON, but I believe this has a lot to do with ser/deser from/to specific POCO classes. Anyway, speed was not on our list of pros/cons for this decision, so YMMV.
Long story short, for the time being I wrote a small dynamic object wrapper that unpacks the JsonElements from System.Text.Json and tries to convert/cast as best as possible. The typical usage is to read the request body as a dynamic object. Again, I'm pretty sure this approach kills any speed gains, but that was not a concern for our use case.
This is the class:
public class ReflectionDynamicObject : DynamicObject {
public JsonElement RealObject { get; set; }
public override bool TryGetMember (GetMemberBinder binder, out object result) {
// Get the property value
var srcData = RealObject.GetProperty (binder.Name);
result = null;
switch (srcData.ValueKind) {
case JsonValueKind.Null:
result = null;
break;
case JsonValueKind.Number:
result = srcData.GetDouble ();
break;
case JsonValueKind.False:
result = false;
break;
case JsonValueKind.True:
result = true;
break;
case JsonValueKind.Undefined:
result = null;
break;
case JsonValueKind.String:
result = srcData.GetString ();
break;
case JsonValueKind.Object:
result = new ReflectionDynamicObject {
RealObject = srcData
};
break;
case JsonValueKind.Array:
result = srcData.EnumerateArray ()
.Select (o => new ReflectionDynamicObject { RealObject = o })
.ToArray ();
break;
}
// Always return true; other exceptions may have already been thrown if needed
return true;
}
}
and this is an example usage, to parse the request body - one part is in a base class for all my WebAPI controllers, that exposes the body as a dynamic object:
[ApiController]
public class WebControllerBase : Controller {
// Other stuff - omitted
protected async Task<dynamic> JsonBody () {
var result = await JsonDocument.ParseAsync (Request.Body);
return new ReflectionDynamicObject {
RealObject = result.RootElement
};
}
}
and can be used in the actual controller like this:
//[...]
[HttpPost ("")]
public async Task<ActionResult> Post () {
var body = await JsonBody ();
var name = (string) body.Name;
//[...]
}
//[...]
If needed, you can integrate parsing for GUIDs or other specific data types as needed - while we all wait for some official / framework-sanctioned solution.
Actual way to parse string in System.Text.Json (.NET Core 3+)
var jsonStr = "{\"id\":\"48e86841-f62c-42c9-ae20-b54ba8c35d6d\"}";
using var doc = JsonDocument.Parse(jsonStr);
var root = doc.RootElement;
var id = root.GetProperty("id").GetGuid();
You can use the following extension method to query data like "xpath"
public static string? JsonQueryXPath(this string value, string xpath, JsonSerializerOptions? options = null) => value.Deserialize<JsonElement>(options).GetJsonElement(xpath).GetJsonElementValue();
public static JsonElement GetJsonElement(this JsonElement jsonElement, string xpath)
{
if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
return default;
string[] segments = xpath.Split(new[] {'.'}, StringSplitOptions.RemoveEmptyEntries);
foreach (var segment in segments)
{
if (int.TryParse(segment, out var index) && jsonElement.ValueKind == JsonValueKind.Array)
{
jsonElement = jsonElement.EnumerateArray().ElementAtOrDefault(index);
if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
return default;
continue;
}
jsonElement = jsonElement.TryGetProperty(segment, out var value) ? value : default;
if (jsonElement.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
return default;
}
return jsonElement;
}
public static string? GetJsonElementValue(this JsonElement jsonElement) => jsonElement.ValueKind != JsonValueKind.Null &&
jsonElement.ValueKind != JsonValueKind.Undefined
? jsonElement.ToString()
: default;
Simple to use as follows
string raw = #"{
""data"": {
""products"": {
""edges"": [
{
""node"": {
""id"": ""gid://shopify/Product/4534543543316"",
""featuredImage"": {
""originalSrc"": ""https://cdn.shopify.com/s/files/1/0286/pic.jpg"",
""id"": ""gid://shopify/ProductImage/146345345339732""
}
}
},
{
""node"": {
""id"": ""gid://shopify/Product/123456789"",
""featuredImage"": {
""originalSrc"": ""https://cdn.shopify.com/s/files/1/0286/pic.jpg"",
""id"": [
""gid://shopify/ProductImage/123456789"",
""gid://shopify/ProductImage/666666666""
]
},
""1"": {
""name"": ""Tuanh""
}
}
}
]
}
}
}";
System.Console.WriteLine(raw2.QueryJsonXPath("data.products.edges.0.node.featuredImage.id"));
I wrote an extension method for this purpose. You can safely use as following:
var jsonElement = JsonSerializer.Deserialize<JsonElement>(json);
var guid = jsonElement.TryGetValue<Guid>("id");
This is the extension class.
public static class JsonElementExtensions
{
private static readonly JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true };
public static T? TryGetValue<T>(this JsonElement element, string propertyName)
{
if (element.ValueKind != JsonValueKind.Object)
{
return default;
}
element.TryGetProperty(propertyName, out JsonElement property);
if (property.ValueKind == JsonValueKind.Undefined ||
property.ValueKind == JsonValueKind.Null)
{
return default;
}
try
{
return property.Deserialize<T>(options);
}
catch (JsonException)
{
return default;
}
}
}
Reason
The reason behind using this extension instead of JsonNode class is because if you need a Controller method accepts just an object without exposing it's model class Asp.Net Core model binding uses JsonElement struct to map the json string. At this point (as far as I know) there is no simple way to convert the JsonElement to JsonNode and when your object can be anything the JsonElement methods will throw exceptions for undefined fields while JsonNode don't.
[HttpPost]
public IActionResult Post(object setupObject)
{
var setup = (JsonElement)setupObject;
var id = setup.TryGetValue<Guid>("id");
var user = setup.TryGetValue<User?>("user");
var account = setup.TryGetValue<Account?>("account");
var payments = setup.TryGetValue<IEnumerable<Payments>?>("payments");
// ...
return Ok();
}
update to .NET Core 3.1 to support
public static dynamic FromJson(this string json, JsonSerializerOptions options = null)
{
if (string.IsNullOrEmpty(json))
return null;
try
{
return JsonSerializer.Deserialize<ExpandoObject>(json, options);
}
catch
{
return null;
}
}
You can also deserialize your json to an object of your target class, and then read its properties as per normal:
var obj = DeSerializeFromStrToObj<ClassToSerialize>(jsonStr);
Console.WriteLine($"Property: {obj.Property}");
where DeSerializeFromStrToObj is a custom class that makes use of reflection to instantiate an object of a targeted class:
public static T DeSerializeFromStrToObj<T>(string json)
{
try
{
var o = (T)Activator.CreateInstance(typeof(T));
try
{
var jsonDict = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
var props = o.GetType().GetProperties();
if (props == null || props.Length == 0)
{
Debug.WriteLine($"Error: properties from target class '{typeof(T)}' could not be read using reflection");
return default;
}
if (jsonDict.Count != props.Length)
{
Debug.WriteLine($"Error: number of json lines ({jsonDict.Count}) should be the same as number of properties ({props.Length})of our class '{typeof(T)}'");
return default;
}
foreach (var prop in props)
{
if (prop == null)
{
Debug.WriteLine($"Error: there was a prop='null' in our target class '{typeof(T)}'");
return default;
}
if (!jsonDict.ContainsKey(prop.Name))
{
Debug.WriteLine($"Error: jsonStr does not refer to target class '{typeof(T)}'");
return default;
}
var value = jsonDict[prop.Name];
Type t = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
object safeValue = value ?? Convert.ChangeType(value, t);
prop.SetValue(o, safeValue, null); // initialize property
}
return o;
}
catch (Exception e2)
{
Debug.WriteLine(e2.Message);
return o;
}
}
catch (Exception e)
{
Debug.WriteLine(e.Message);
return default;
}
}
You can test your jsons for example here
Here you find a complete working example with different ways of serialization and deserialization that might be of interest for you and/or future readers:
using System;
using System.Collections.Generic;
using System.Text.Json;
using static Json_Tests.JsonHelpers;
namespace Json_Tests
{
public class Class1
{
public void Test()
{
var obj1 = new ClassToSerialize();
var jsonStr = obj1.ToString();
// if you have the class structure for the jsonStr (for example, if you have created the jsonStr yourself from your code):
var obj2 = DeSerializeFromStrToObj<ClassToSerialize>(jsonStr);
Console.WriteLine($"{nameof(obj2.Name)}: {obj2.Name}");
// if you do not have the class structure for the jsonStr (forexample, jsonStr comes from a 3rd party service like the web):
var obj3 = JsonSerializer.Deserialize<object>(jsonStr) as JsonElement?;
var propName = nameof(obj1.Name);
var propVal1 = obj3?.GetProperty("Name");// error prone
Console.WriteLine($"{propName}: {propVal1}");
JsonElement propVal2 = default;
obj3?.TryGetProperty("Name", out propVal2);// error prone
Console.WriteLine($"{propName}: {propVal2}");
var obj4 = DeSerializeFromStrToDict(jsonStr);
foreach (var pair in obj4)
Console.WriteLine($"{pair.Key}: {pair.Value}");
}
}
[Serializable]
public class ClassToSerialize
{
// important: properties must have at least getters
public string Name { get; } = "Paul";
public string Surname{ get; set; } = "Efford";
public override string ToString() => JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true });
}
public static class JsonHelpers
{
/// <summary>
/// to use if you do not have the class structure for the jsonStr (forexample, jsonStr comes from a 3rd party service like the web)
/// </summary>
public static Dictionary<string, string> DeSerializeFromStrToDict(string json)
{
try
{
return JsonSerializer.Deserialize<Dictionary<string, string>>(json);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return new Dictionary<string, string>(); // return empty
}
}
/// <summary>
/// to use if you have the class structure for the jsonStr (for example, if you have created the jsonStr yourself from your code)
/// </summary>
public static T DeSerializeFromStrToObj<T>(string json) // see this: https://json2csharp.com/#
{
try
{
var o = (T)Activator.CreateInstance(typeof(T));
try
{
var jsonDict = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
var props = o.GetType().GetProperties();
if (props == null || props.Length == 0)
{
Console.WriteLine($"Error: properties from target class '{typeof(T)}' could not be read using reflection");
return default;
}
if (jsonDict.Count != props.Length)
{
Console.WriteLine($"Error: number of json lines ({jsonDict.Count}) should be the same as number of properties ({props.Length})of our class '{typeof(T)}'");
return default;
}
foreach (var prop in props)
{
if (prop == null)
{
Console.WriteLine($"Error: there was a prop='null' in our target class '{typeof(T)}'");
return default;
}
if (!jsonDict.ContainsKey(prop.Name))
{
Console.WriteLine($"Error: jsonStr does not refer to target class '{typeof(T)}'");
return default;
}
var value = jsonDict[prop.Name];
Type t = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
object safeValue = value ?? Convert.ChangeType(value, t);
prop.SetValue(o, safeValue, null); // initialize property
}
return o;
}
catch (Exception e2)
{
Console.WriteLine(e2.Message);
return o;
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return default;
}
}
}
}
I'm having an unusual problem. It might not be a very realistic scenario, but this is what I have gotten myself into, so please bear with me.
I have an API that returns Json and I'm using Json.NET to process the Json response. The problem is that the API can return a number of things and I have to be able to deserialize the response the following way:
The API can return a single Json object. In this case I have to deserialize it into an ExpandoObject and put it into a List<dynamic>.
The API can return null and undefined and the alike, in which case I have to return an empty list.
The API can return a single primitive value, like a Json string or a Json float. In this case I have to deserialize it into the appropriate .NET type, put it in a List<dynamic> and return that.
The API can return a Json array. In this case I have to deserialize the array into a List<dynamic>:
The elements in the array can be Json objects, in which case I have to deserialize them into ExpandoObject again, and put them in the list.
The elements can also be primitive values. In this case I have to deserialize them into the proper .NET type and put them in the list.
Based on my research, here's what I have come up so far:
protected IQueryable<dynamic> TestMethod(string r)
{
using (StringReader sr = new StringReader(r))
using (JsonTextReader reader = new JsonTextReader(sr))
{
if (!reader.Read())
{
return new List<ExpandoObject>().AsQueryable();
}
switch (reader.TokenType)
{
case JsonToken.None:
case JsonToken.Null:
case JsonToken.Undefined:
return new List<ExpandoObject>().AsQueryable();
case JsonToken.StartArray:
return JsonConvert.DeserializeObject<List<ExpandoObject>>(r).AsQueryable();
case JsonToken.StartObject:
return DeserializeAs<ExpandoObject>(r);
case JsonToken.Integer:
return DeserializeAs<long>(r);
case JsonToken.Float:
return DeserializeAs<double>(r);
// other Json primitives deserialized here
case JsonToken.StartConstructor:
// listing other not processed tokens
default:
throw new InvalidOperationException($"Token {reader.TokenType} cannot be the first token in the result");
}
}
}
private IQueryable<dynamic> DeserializeAs<T>(string r)
{
T instance = JsonConvert.DeserializeObject<T>(r);
return new List<dynamic>() { instance }.AsQueryable();
}
The problem is with the last bullet point. In the switch-case, when the deserializer encounters StartArray token, it tries to deserialize the json into a List<ExpandoObject>, but if the array contains integers, they cannot be deserialized into ExpandoObject.
Can anyone give me a simple solution to support both scenarios: array of Json objects to List<ExpandoObject> and array of Json primitives to their respective list?
Since Json.NET is licensed under the MIT license, you could adapt the logic of ExpandoObjectConverter to fit your needs, and create the following method:
public static class JsonExtensions
{
public static IQueryable<object> ReadJsonAsDynamicQueryable(string json, JsonSerializerSettings settings = null)
{
var serializer = JsonSerializer.CreateDefault(settings);
using (StringReader sr = new StringReader(json))
using (JsonTextReader reader = new JsonTextReader(sr))
{
var root = JsonExtensions.ReadJsonAsDynamicQueryable(reader, serializer);
return root;
}
}
public static IQueryable<dynamic> ReadJsonAsDynamicQueryable(JsonReader reader, JsonSerializer serializer)
{
dynamic obj;
if (!TryReadJsonAsDynamic(reader, serializer, out obj) || obj == null)
return Enumerable.Empty<dynamic>().AsQueryable();
var list = obj as IList<dynamic> ?? new [] { obj };
return list.AsQueryable();
}
public static bool TryReadJsonAsDynamic(JsonReader reader, JsonSerializer serializer, out dynamic obj)
{
// Adapted from:
// https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Converters/ExpandoObjectConverter.cs
// License:
// https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md
if (reader.TokenType == JsonToken.None)
if (!reader.Read())
{
obj = null;
return false;
}
switch (reader.TokenType)
{
case JsonToken.StartArray:
var list = new List<dynamic>();
ReadList(reader,
(r) =>
{
dynamic item;
if (TryReadJsonAsDynamic(reader, serializer, out item))
list.Add(item);
});
obj = list;
return true;
case JsonToken.StartObject:
obj = serializer.Deserialize<ExpandoObject>(reader);
return true;
default:
if (reader.TokenType.IsPrimitiveToken())
{
obj = reader.Value;
return true;
}
else
{
throw new JsonSerializationException("Unknown token: " + reader.TokenType);
}
}
}
static void ReadList(this JsonReader reader, Action<JsonReader> readValue)
{
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.Comment:
break;
default:
readValue(reader);
break;
case JsonToken.EndArray:
return;
}
}
throw new JsonSerializationException("Unexpected end when reading List.");
}
public static bool IsPrimitiveToken(this JsonToken token)
{
switch (token)
{
case JsonToken.Integer:
case JsonToken.Float:
case JsonToken.String:
case JsonToken.Boolean:
case JsonToken.Undefined:
case JsonToken.Null:
case JsonToken.Date:
case JsonToken.Bytes:
return true;
default:
return false;
}
}
}
Then use it like:
protected IQueryable<dynamic> TestMethod(string r)
{
return JsonExtensions.ReadJsonAsDynamicQueryable(r);
}
Or, you could call ReadJsonAsDynamicQueryable from within the ReadJson() method of a custom JsonConverter that you create.
Sample fiddle.
I have following class:
public class MainClass
{
public static MainClass[] array = new MainClass[1]
{
new MainClass
{
subClass = new SubClass[2]
{
new SubClass
{
variable1 = "my value"
},
new SubClass
{
variable1 = "my value"
}
}
}
};
public SubClass[] subClass;
[DataContract]
public class SubClass
{
public string variable1 = "default value";
[DataMember] // because only variable2 should be saved in json
public string variable2 = "default value";
}
}
which I save as follows:
File.WriteAllText("data.txt", JsonConvert.SerializeObject(new
{
MainClass.array
}, new JsonSerializerSettings { Formatting = Formatting.Indented }));
data.txt:
{
"array": [
{
"subClass": [
{
"variable2": "value from json"
},
{
"variable2": "value from json"
}
]
}
]
}
then I deserialize and populate my object like this:
JObject json = JObject.Parse(File.ReadAllText("data.txt"));
if (json["array"] != null)
{
for (int i = 0, len = json["array"].Count(); i < len; i++)
{
using (var sr = json["array"][i].CreateReader())
{
JsonSerializer.CreateDefault().Populate(sr, MainClass.array[i]);
}
}
}
however, when I print following variables:
Console.WriteLine(MainClass.array[0].subClass[0].variable1);
Console.WriteLine(MainClass.array[0].subClass[0].variable2);
Console.WriteLine(MainClass.array[0].subClass[1].variable1);
Console.WriteLine(MainClass.array[0].subClass[1].variable2);
then output of it is:
default value
value from json
default value
value from json
but instead of "default value" there should be "my value" because that is what I used while creating an instance of class and JsonSerializer should only populate the object with values from json.
How do I properly populate the whole object without resetting its properties which are not included in json?
It looks as though JsonSerializer.Populate() lacks the MergeArrayHandling setting that is available for JObject.Merge(). Through testing I have found that:
Populating members that are arrays or some other type of read-only collection seems to work like MergeArrayHandling.Replace.
This is the behavior you are experiencing -- the existing array and all the items therein are being discarded and replaced with a fresh array containing newly constructed items that have default values. In contrast, you require MergeArrayHandling.Merge: Merge array items together, matched by index.
Populating members that are read/write collections such as List<T> seems to work like MergeArrayHandling.Concat.
It seems reasonable to request an enhancement that Populate() support this setting -- though I don't know how easy it would be to implement. At the minimum the documentation for Populate() should explain this behavior.
In the meantime, here's a custom JsonConverter with the necessary logic to emulate the behavior of MergeArrayHandling.Merge:
public class ArrayMergeConverter<T> : ArrayMergeConverter
{
public override bool CanConvert(Type objectType)
{
return objectType.IsArray && objectType.GetArrayRank() == 1 && objectType.GetElementType() == typeof(T);
}
}
public class ArrayMergeConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (!objectType.IsArray)
throw new JsonSerializationException(string.Format("Non-array type {0} not supported.", objectType));
var contract = (JsonArrayContract)serializer.ContractResolver.ResolveContract(objectType);
if (contract.IsMultidimensionalArray)
throw new JsonSerializationException("Multidimensional arrays not supported.");
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.StartArray)
throw new JsonSerializationException(string.Format("Invalid start token: {0}", reader.TokenType));
var itemType = contract.CollectionItemType;
var existingList = existingValue as IList;
IList list = new List<object>();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.Comment:
break;
case JsonToken.Null:
list.Add(null);
break;
case JsonToken.EndArray:
var array = Array.CreateInstance(itemType, list.Count);
list.CopyTo(array, 0);
return array;
default:
// Add item to list
var existingItem = existingList != null && list.Count < existingList.Count ? existingList[list.Count] : null;
if (existingItem == null)
{
existingItem = serializer.Deserialize(reader, itemType);
}
else
{
serializer.Populate(reader, existingItem);
}
list.Add(existingItem);
break;
}
}
// Should not come here.
throw new JsonSerializationException("Unclosed array at path: " + reader.Path);
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanConvert(Type objectType)
{
throw new NotImplementedException();
}
}
Then add the converter to your subClass member as follows:
[JsonConverter(typeof(ArrayMergeConverter))]
public SubClass[] subClass;
Or, if you don't want to add Json.NET attributes to your data model, you can add it in serializer settings:
var settings = new JsonSerializerSettings
{
Converters = new[] { new ArrayMergeConverter<MainClass.SubClass>() },
};
JsonSerializer.CreateDefault(settings).Populate(sr, MainClass.array[i]);
The converter is specifically designed for arrays but a similar converter could easily be created for read/write collections such as List<T>.
How can I get Json.NET to deserialize into dynamic objects but still do reference resolution?
dynamic d=JsonConvert.DeserializeObject<ExpandoObject>(...) just as
dynamic d=JsonConvert.DeserializeObject(...) returns a dynamic object but they don't resolve the $ref and $id parts. (An ExpandoObject eo for example will only have eo["$ref"]="..." and doesn't have the properties it should have because it's not the same as the $id-Object)
What I've found out is that I need the contract resolver resolve to a dynamic contract - which ExpandoObject only does if I explicitly tell Json.NET with a custom ContractResolver.
Still It seems the ExpandoObject is parsed with it's own Converter and it fails again.
I've tried a custom class inheriting from IDynamicMetaObjectProvider which resulted in an infinite loop and didn't seem like the right thing. I would actually expect some easy solution to get ExpandoObject to have reference resolution.
Any help?
Since Json.NET is open source and its MIT license allows modification, the easiest solution may be to adapt its ExpandoObjectConverter to your needs:
/// <summary>
/// Converts an ExpandoObject to and from JSON, handling object references.
/// </summary>
public class ObjectReferenceExpandoObjectConverter : JsonConverter
{
// Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Converters/ExpandoObjectConverter.cs
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// can write is set to false
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return ReadValue(serializer, reader);
}
private object ReadValue(JsonSerializer serializer, JsonReader reader)
{
while (reader.TokenType == JsonToken.Comment)
{
if (!reader.Read())
throw reader.CreateException("Unexpected end when reading ExpandoObject.");
}
switch (reader.TokenType)
{
case JsonToken.StartObject:
return ReadObject(serializer, reader);
case JsonToken.StartArray:
return ReadList(serializer, reader);
default:
if (JsonTokenUtils.IsPrimitiveToken(reader.TokenType))
return reader.Value;
throw reader.CreateException("Unexpected token when converting ExpandoObject");
}
}
private object ReadList(JsonSerializer serializer, JsonReader reader)
{
IList<object> list = new List<object>();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.Comment:
break;
default:
object v = ReadValue(serializer, reader);
list.Add(v);
break;
case JsonToken.EndArray:
return list;
}
}
throw reader.CreateException("Unexpected end when reading ExpandoObject.");
}
private object ReadObject(JsonSerializer serializer, JsonReader reader)
{
IDictionary<string, object> expandoObject = null;
object referenceObject = null;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
string propertyName = reader.Value.ToString();
if (!reader.Read())
throw new InvalidOperationException("Unexpected end when reading ExpandoObject.");
object v = ReadValue(serializer, reader);
if (propertyName == "$ref")
{
var id = (v == null ? null : Convert.ToString(v, CultureInfo.InvariantCulture));
referenceObject = serializer.ReferenceResolver.ResolveReference(serializer, id);
}
else if (propertyName == "$id")
{
var id = (v == null ? null : Convert.ToString(v, CultureInfo.InvariantCulture));
serializer.ReferenceResolver.AddReference(serializer, id, (expandoObject ?? (expandoObject = new ExpandoObject())));
}
else
{
(expandoObject ?? (expandoObject = new ExpandoObject()))[propertyName] = v;
}
break;
case JsonToken.Comment:
break;
case JsonToken.EndObject:
if (referenceObject != null && expandoObject != null)
throw reader.CreateException("ExpandoObject contained both $ref and real data");
return referenceObject ?? expandoObject;
}
}
throw reader.CreateException("Unexpected end when reading ExpandoObject.");
}
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(ExpandoObject));
}
public override bool CanWrite
{
get { return false; }
}
}
public static class JsonTokenUtils
{
// Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Utilities/JsonTokenUtils.cs
public static bool IsPrimitiveToken(this JsonToken token)
{
switch (token)
{
case JsonToken.Integer:
case JsonToken.Float:
case JsonToken.String:
case JsonToken.Boolean:
case JsonToken.Undefined:
case JsonToken.Null:
case JsonToken.Date:
case JsonToken.Bytes:
return true;
default:
return false;
}
}
}
public static class JsonReaderExtensions
{
public static JsonSerializationException CreateException(this JsonReader reader, string format, params object[] args)
{
// Adapted from https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/JsonPosition.cs
var lineInfo = reader as IJsonLineInfo;
var path = (reader == null ? null : reader.Path);
var message = string.Format(CultureInfo.InvariantCulture, format, args);
if (!message.EndsWith(Environment.NewLine, StringComparison.Ordinal))
{
message = message.Trim();
if (!message.EndsWith(".", StringComparison.Ordinal))
message += ".";
message += " ";
}
message += string.Format(CultureInfo.InvariantCulture, "Path '{0}'", path);
if (lineInfo != null && lineInfo.HasLineInfo())
message += string.Format(CultureInfo.InvariantCulture, ", line {0}, position {1}", lineInfo.LineNumber, lineInfo.LinePosition);
message += ".";
return new JsonSerializationException(message);
}
}
And then use it like:
var settings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects, ReferenceLoopHandling = ReferenceLoopHandling.Serialize };
settings.Converters.Add(new ObjectReferenceExpandoObjectConverter());
dynamic d = JsonConvert.DeserializeObject<ExpandoObject>(json, settings);
The way I did it now is with a postprocessing step and a recursive function that's doing its own reference saving and rewiring:
private static void Reffing(this IDictionary<string, object> current, Action<object> exchange,IDictionary<string, object> refdic)
{
object value;
if(current.TryGetValue("$ref", out value))
{
if(!refdic.TryGetValue((string) value, out value))
throw new Exception("ref not found ");
if (exchange != null)
exchange(value);
return;
}
if (current.TryGetValue("$id", out value))
{
refdic[(string) value] = current;
}
foreach (var kvp in current.ToList())
{
if (kvp.Key.StartsWith("$"))
continue;
var expandoObject = kvp.Value as ExpandoObject;
if(expandoObject != null)
Reffing(expandoObject,o => current[kvp.Key]=o,refdic);
var list = kvp.Value as IList<object>;
if (list == null) continue;
for (var i = 0; i < list.Count; i++)
{
var lEO = list[i] as ExpandoObject;
if(lEO!=null)
Reffing(lEO,o => list[i]=o,refdic);
}
}
}
used as:
var test = JsonConvert.DeserializeObject<ExpandoObject>(...);
var dictionary = new Dictionary<string, object>();
Reffing(test,null,dictionary);