I currently have a list builder from a separate class:
public class psuedoMe {
public string relName { get; set; }
public List<string> lstName { get; set; }
}
I have a function that populates this, but then writes it to Json using Newtonsoft.Json.JsonWriter:
private static string returnJson(List<psuedoMe> sentList)
{
StringBuilder jsonSB = new StringBuilder();
StringWriter jsonSW = new StringWriter(jsonSB);
using (JsonWriter jsonWrite = new JsonTextWriter(jsonSW))
{
jsonWrite.WriteStartArray();
foreach (psuedoMe sentItem in sentList)
{
jsonWrite.WriteStartObject();
foreach (System.Reflection.PropertyInfo propInfo in sentItem.GetType().GetProperties())
{
jsonWrite.WritePropertyName(propInfo.Name);
jsonWrite.WriteValue(propInfo.GetValue(sentItem, null));
}
jsonWrite.WriteEndObject();
}
jsonWrite.WriteEndArray();
}
return jsonSB.ToString();
}
However I am receiving an error when it tries to write the public List<string> lstName into jsonSB. I've tried excluding only the lstName from the JsonWriter however this only then loops through the list and doesn't write it to the jsonSB at the end.
Is there anyway of using the above returnJson to write to the list of strings?
There error I get is: Newtonsoft.Json.JsonWriterException: Unsupported type: System.Collections.Generic.List1[System.String].
Which version of JSON.NET you're using? Here's the source code of JsonWriter class's WriteValue method
public virtual void WriteValue(object value)
{
if (value == null)
{
WriteNull();
return;
}
else if (value is IConvertible)
{
IConvertible convertible = value as IConvertible;
switch (convertible.GetTypeCode())
{
case TypeCode.String:
WriteValue(convertible.ToString(CultureInfo.InvariantCulture));
return;
case TypeCode.Char:
WriteValue(convertible.ToChar(CultureInfo.InvariantCulture));
return;
case TypeCode.Boolean:
WriteValue(convertible.ToBoolean(CultureInfo.InvariantCulture));
return;
case TypeCode.SByte:
WriteValue(convertible.ToSByte(CultureInfo.InvariantCulture));
return;
case TypeCode.Int16:
WriteValue(convertible.ToInt16(CultureInfo.InvariantCulture));
return;
case TypeCode.UInt16:
WriteValue(convertible.ToUInt16(CultureInfo.InvariantCulture));
return;
case TypeCode.Int32:
WriteValue(convertible.ToInt32(CultureInfo.InvariantCulture));
return;
case TypeCode.Byte:
WriteValue(convertible.ToByte(CultureInfo.InvariantCulture));
return;
case TypeCode.UInt32:
WriteValue(convertible.ToUInt32(CultureInfo.InvariantCulture));
return;
case TypeCode.Int64:
WriteValue(convertible.ToInt64(CultureInfo.InvariantCulture));
return;
case TypeCode.UInt64:
WriteValue(convertible.ToUInt64(CultureInfo.InvariantCulture));
return;
case TypeCode.Single:
WriteValue(convertible.ToSingle(CultureInfo.InvariantCulture));
return;
case TypeCode.Double:
WriteValue(convertible.ToDouble(CultureInfo.InvariantCulture));
return;
case TypeCode.DateTime:
WriteValue(convertible.ToDateTime(CultureInfo.InvariantCulture));
return;
case TypeCode.Decimal:
WriteValue(convertible.ToDecimal(CultureInfo.InvariantCulture));
return;
case TypeCode.DBNull:
WriteNull();
return;
}
}
#if !PocketPC && !NET20
else if (value is DateTimeOffset)
{
WriteValue((DateTimeOffset)value);
return;
}
#endif
else if (value is byte[])
{
WriteValue((byte[])value);
return;
}
throw new ArgumentException("Unsupported type: {0}. Use the JsonSerializer class to get the object's JSON representation.".FormatWith(CultureInfo.InvariantCulture, value.GetType()));
}
As you can see, it doesn't have support for List<T>. So for List<T> it will throw Unsupported type exception, and even in exception message it is suggesting to use JsonSerializer to get object's JSON representation.
Related
I have the following json string:
[
{
"Key":"A",
"Value":null
},
{
"Key":"B",
"Value":"18"
},
{
"Key":"C",
"Value":"False"
},
{
"Key":"D",
"Value":"BOB"
}
]
I would like to be able to deserialize into the following objects:
public class ModelOne
{
public int? A { get; set; }
public int B { get; set;}
}
public class ModelTwo
{
public bool C { get; set; }
public string D { get; set; }
}
We thought about using var model = JsonConvert.DeserializeObject<ModelOne>(json); but clearly the json string is a list of Key and Value so that wouldn't work.
In an ideal world we would like to parse the json and match the Key to the Property Name and set the Value according to the property type. We could use a similar function to the above which accepts an anonymous type we're just not sure where to start so would be very greatful for some feedback and or assistance.
Thanks in advance.
EDIT:
The json array represents some data points we receive from an external api call.
ModelOne and ModelTwo are each view models in our MVC project we would like to pre-populate.
Thanks very much for all of your comments but notably both of #mjwills and #Heretic Monkey you really helped.
In the (end against my better judgement) I decided to use a little reflection.
public T ConvertDataMapToModel<T>(T item, List<Data> list)
{
Type itemType = typeof(T);
var response = (T)item;
var props = response.GetType().GetProperties();
foreach (var prop in props)
{
string propName = prop.Name;
string listValue = (string)(from c in list where c.Key == prop.Name select c.Value).FirstOrDefault();
if (!string.IsNullOrWhiteSpace(listValue) && !string.IsNullOrEmpty(listValue))
{
PropertyInfo pInstance = itemType.GetProperty(propName);
Type pInstancePropertyType = pInstance.PropertyType;
if (pInstancePropertyType.IsGenericType && pInstancePropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
pInstancePropertyType = pInstancePropertyType.GetGenericArguments()[0];
}
TypeCode typeCode = Type.GetTypeCode(pInstancePropertyType);
switch (typeCode)
{
case TypeCode.Boolean:
pInstance.SetValue(response, Convert.ToBoolean(listValue));
break;
case TypeCode.Byte:
pInstance.SetValue(response, Convert.ToByte(listValue));
break;
case TypeCode.Char:
pInstance.SetValue(response, Convert.ToChar(listValue));
break;
case TypeCode.DateTime:
pInstance.SetValue(response, Convert.ToDateTime(listValue));
break;
case TypeCode.DBNull:
pInstance.SetValue(response, Convert.DBNull);
break;
case TypeCode.Decimal:
pInstance.SetValue(response, Convert.ToDecimal(listValue));
break;
case TypeCode.Double:
pInstance.SetValue(response, Convert.ToDouble(listValue));
break;
case TypeCode.Empty:
pInstance.SetValue(response, "");
break;
case TypeCode.Int16:
pInstance.SetValue(response, Convert.ToInt16(listValue));
break;
case TypeCode.Int32:
pInstance.SetValue(response, Convert.ToInt32(listValue));
break;
case TypeCode.Int64:
pInstance.SetValue(response, Convert.ToInt64(listValue));
break;
case TypeCode.SByte:
pInstance.SetValue(response, Convert.ToSByte(listValue));
break;
case TypeCode.Single:
pInstance.SetValue(response, Convert.ToSingle(listValue));
break;
case TypeCode.String:
pInstance.SetValue(response, Convert.ToString(listValue));
break;
case TypeCode.UInt16:
pInstance.SetValue(response, Convert.ToUInt16(listValue));
break;
case TypeCode.UInt32:
pInstance.SetValue(response, Convert.ToUInt32(listValue));
break;
case TypeCode.UInt64:
pInstance.SetValue(response, Convert.ToUInt64(listValue));
break;
}
}
}
return response;
}
I receive some not really ISO conform Json content from an api. The boolean values are uppercase instead of lower case.
{ "Bool": False }
Initially, I thought that should be easy to solve by using a custom JsonConverter like shown in how to get newtonsoft to deserialize yes and no to boolean.
But it looks like the JsonConverter.ReadJson method is never called. I think the reason is, that the value False is not in quotes and thus JsonTextReader never calls the converter and creates the exception.
What would be the best way to handle that scenario?
public class BoolTests
{
public class A
{
[JsonConverter(typeof(CaseIgnoringBooleanConverter))]
public bool Bool { get; set; }
}
[Theory]
[InlineData(false, "{'Bool': false}")] //ok
[InlineData(false, "{'Bool': 'False'}")] // ok
[InlineData(false, "{'Bool': False")] // fails
public void CasingMatters(bool expected, string json)
{
var actual = JsonConvert.DeserializeObject<A>(json);
Assert.Equal(expected, actual.Bool);
}
}
// taken from https://gist.github.com/randyburden/5924981
public class CaseIgnoringBooleanConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
switch (reader.Value.ToString().ToUpperInvariant().Trim())
{
case "TRUE":
return true;
case "FALSE":
return false;
}
// If we reach here, we're pretty much going to throw an error so let's let Json.NET throw it's pretty-fied error message.
return new JsonSerializer().Deserialize(reader, objectType);
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(bool);
}
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Unfortunately, as you've discovered, invalid json is invalid, and thus not handled by normal and common json (de)serializers, such as Json.net.
Using converters and strategy settings for the deserializers will not work either as they're meant to handle things like empty-objects-returned-as-arrays or name conversion/case handling.
One naive solution would be to do a simple string replace, like
string json = invalidJson.Replace("False", "false");
This, however, has some problems:
You need to read the entire invalid json into memory, as well as create a fixed copy of it, which means you will have two entire copies of the data in memory, one bad and one better.
It would replace False inside strings as well. This may not be a problem with your data but wouldn't be easy to handle with the above approach.
A different approach would be to write a basic tokenizer that understands rudimentary JSON syntax, such as strings and numbers and identifiers, and go through the file token by token, replacing the bad identifiers. This would fix problem 2, but depending on the solution might need a more complex implementation to fix problem 1 with the memory.
A simple attempt at creating a TextReader that can be used, that will fix identifiers as they're found and otherwise understands rudimentary JSON tokens is posted below.
Note the following:
It is not really performant. It allocates temporary buffers all the time. You might want to look into "buffer renting" to handle this approach somewhat better, or even just stream directly to the buffer.
It doesn't handle numbers, because I stopped writing code at that point. I left this as an excercise. A basic number handling can be written because you're not really validating that the file is having valid JSON, so anything that will grab enough characters to constitute a number can be added.
I did not test this with really big files, only with the small example file. I replicated a List<Test> with 9.5MB of text, and it works for that.
I did not test all JSON syntax. There may be characters that should be handled but isn't. If you end up using this, create LOTS of tests!
What it does, however, is fix the invalid JSON according to the identifier(s) you've posted, and it does so in a streaming manner. This should thus be usable no matter how big a JSON file you have.
Anyway, here's the code, again note the exception regarding numbers:
void Main()
{
using (var file = File.OpenText(#"d:\temp\test.json"))
using (var fix = new MyFalseFixingTextReader(file))
{
var reader = new JsonTextReader(fix);
var serializer = new JsonSerializer();
serializer.Deserialize<Test>(reader).Dump();
}
}
public class MyFalseFixingTextReader : TextReader
{
private readonly TextReader _Reader;
private readonly StringBuilder _Buffer = new StringBuilder(32768);
public MyFalseFixingTextReader(TextReader reader) => _Reader = reader;
public override void Close()
{
_Reader.Close();
base.Close();
}
public override int Read(char[] buffer, int index, int count)
{
TryFillBuffer(count);
int amountToCopy = Math.Min(_Buffer.Length, count);
_Buffer.CopyTo(0, buffer, index, amountToCopy);
_Buffer.Remove(0, amountToCopy);
return amountToCopy;
}
private (bool more, char c) TryReadChar()
{
int i = _Reader.Read();
if (i < 0)
return (false, default);
return (true, (char)i);
}
private (bool more, char c) TryPeekChar()
{
int i = _Reader.Peek();
if (i < 0)
return (false, default);
return (true, (char)i);
}
private void TryFillBuffer(int count)
{
if (_Buffer.Length >= count)
return;
while (_Buffer.Length < count)
{
var (more, c) = TryPeekChar();
if (!more)
break;
switch (c)
{
case '{':
case '}':
case '[':
case ']':
case '\r':
case '\n':
case ' ':
case '\t':
case ':':
case ',':
_Reader.Read();
_Buffer.Append(c);
break;
case '"':
_Buffer.Append(GrabString());
break;
case char letter when char.IsLetter(letter):
var identifier = GrabIdentifier();
_Buffer.Append(ReplaceFaultyIdentifiers(identifier));
break;
case char startOfNumber when startOfNumber == '-' || (startOfNumber >= '0' && startOfNumber <= '9'):
_Buffer.Append(GrabNumber());
break;
default:
throw new InvalidOperationException($"Unable to cope with character '{c}' (0x{((int)c).ToString("x2")})");
}
}
}
private string ReplaceFaultyIdentifiers(string identifier)
{
switch (identifier)
{
case "False":
return "false";
case "True":
return "true";
case "Null":
return "null";
default:
return identifier;
}
}
private string GrabNumber()
{
throw new NotImplementedException("Left as an excercise");
// See https://www.json.org/ for the syntax
}
private string GrabIdentifier()
{
var result = new StringBuilder();
while (true)
{
int i = _Reader.Peek();
if (i < 0)
break;
char c = (char)i;
if (char.IsLetter(c))
{
_Reader.Read();
result.Append(c);
}
else
break;
}
return result.ToString();
}
private string GrabString()
{
_Reader.Read();
var result = new StringBuilder();
result.Append('"');
while (true)
{
var (more, c) = TryReadChar();
if (!more)
return result.ToString();
switch (c)
{
case '"':
result.Append(c);
return result.ToString();
case '\\':
result.Append(c);
(more, c) = TryReadChar();
if (!more)
return result.ToString();
switch (c)
{
case 'u':
result.Append(c);
for (int index = 1; index <= 4; index++)
{
(more, c) = TryReadChar();
if (!more)
return result.ToString();
result.Append(c);
}
break;
default:
result.Append(c);
break;
}
break;
default:
result.Append(c);
break;
}
}
}
}
public class Test
{
public bool False1 { get; set; }
public bool False2 { get; set; }
public bool False3 { get; set; }
}
Example file:
{
"false1": false,
"false2": "false",
"false3": False
}
Output (from LINQPad):
As said Lasse :
Invalid json should be fixed at the source.
If you really need to parse it as it is, you could replace False by "False" (as suggested by #Sinatr) if you want it as a string or false if you want it as a bool.
// If you want a string
json.Replace("False", "\"False\"");
// If you want a bool
json.Replace("False", "false");
One problem would be if a key or another value contains the "False" pattern.
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.
The only way I've been able to do it is through the reflection-based code below. I can't believe there's not an "easier NetSuite way" to do this though? Am I missing something basic?
After I perform a search on custom objects I get back an array of Record[], this can then be looped through and each item casted to a CustomObject.
The properties of the custom object are stored in the CustomRecord's customFieldList but the values are not immediately accessible you have to cast those the their real NetSuite type (like LongCustomFieldRef, DoubleCustomFieldRef, BooleanCustomFieldRef, StringCustomFieldRef, etc).
In order to not have to bother with this mess to get nice clean objects on my side I decided on the approach below:
Create classes with property names that match (including case) the NetSuite names and inherits from NetSuiteBase (defined below)
public class MyNetSuiteObject : NetSuiteBase //<-- Note base class
{
public string myProperty1 { get; set; }
public bool myProperty2 { get; set; }
public int myProperty3 { get; set; }
public static MyNetSuiteObject FromCustomSearchRecord(CustomRecord customRecord)
{
var ret = new MyNetSuiteObject();
ret.AssignProperties(customRecord);
return ret;
}
}
Create a base class which will inspect CustomRecords and apply property values to the .NET classes
public class NetSuiteBase
{
public void AssignProperties(CustomRecord customRecord)
{
var classProps = this.GetType().GetProperties();
foreach (var prop in classProps)
{
var propName = prop.Name;
var propValue = prop.GetValue(this, null);
//get the matching CustomFieldRef out of the customFieldList for the CustomRecord which matches our current property name
var myCustomFieldRef = customRecord.customFieldList.Where(c => c.scriptId == propName).FirstOrDefault();
if (myCustomFieldRef == null) continue;
//we can't get the value out until we cast the CustomFieldRef to its "actual" type.
var custType = myCustomFieldRef.GetType().Name;
switch (custType)
{
case "LongCustomFieldRef":
TrySetProperty(prop, ((LongCustomFieldRef)myCustomFieldRef).value.ToString());
break;
case "DoubleCustomFieldRef":
TrySetProperty(prop, ((DoubleCustomFieldRef)myCustomFieldRef).value.ToString());
break;
case "BooleanCustomFieldRef":
TrySetProperty(prop, ((BooleanCustomFieldRef)myCustomFieldRef).value.ToString());
break;
case "StringCustomFieldRef":
TrySetProperty(prop, ((StringCustomFieldRef)myCustomFieldRef).value.ToString());
break;
case "DateCustomFieldRef":
TrySetProperty(prop, ((DateCustomFieldRef)myCustomFieldRef).value.ToString());
break;
case "SelectCustomFieldRef":
TrySetProperty(prop, ((SelectCustomFieldRef)myCustomFieldRef).value.name.ToString());
break;
case "MultiSelectCustomFieldRef":
TrySetProperty(prop, ((MultiSelectCustomFieldRef)myCustomFieldRef).value.ToString());
break;
default:
Console.WriteLine("Unknown type: " + myCustomFieldRef.internalId);
break;
}
}
}
//Some of the NetSuite properties are represented as strings (I'm looking at you BOOLs), so we pass all the values from above
//as strings and them process/attempt casts
private void TrySetProperty(PropertyInfo prop, string value)
{
value = value.ToLower().Trim();
if (prop.PropertyType == typeof(string))
{
prop.SetValue(this, value);
return;
}
if (prop.PropertyType == typeof(bool))
{
if (value == "yes") value = "true";
if (value == "no") value = "false";
if (value == "1") value = "true";
if (value == "0") value = "false";
bool test;
if (bool.TryParse(value, out test))
{
prop.SetValue(this, test);
return;
}
}
if (prop.PropertyType == typeof(int))
{
int test;
if (int.TryParse(value, out test))
{
prop.SetValue(this, test);
return;
}
}
if (prop.PropertyType == typeof(double))
{
double test;
if (double.TryParse(value, out test))
{
prop.SetValue(this, test);
return;
}
}
if (prop.PropertyType == typeof(decimal))
{
decimal test;
if (decimal.TryParse(value, out test))
{
prop.SetValue(this, test);
return;
}
}
}
}
After performing a NetSuite search on custom objects, loop through the results and use the above classes to convert NetSuite result to a .NET class
for (int i = 0, j = 0; i < records.Length; i++, j++)
{
customRecord = (CustomRecord)records[i];
var myNetSuiteObject = MyNetSuiteObject.FromCustomSearchRecord(customRecord);
}
Is there some other "NetSuite way" to accomplish what I have above?
No, there is no "NetSuite" way. What you've done is far above and beyond the call of duty, if you ask me, and it's amazing. The NetSuite/SuiteTalk WSDL is just HORRENDOUS, and so many bad choices were made in the design process that I'm shocked that it was released as-is to developers without any questions raised. Below is one other example.
This is from the documentation for their SuiteTalk course, which reveals that when parsing the results of an Advanced search, they contain within them SearchRowBasic objects. Well within each SearchRowBasic object there are multiple fields. To access those fields' values, you have to take the first element of a SearchColumnField array. Why would you DO this? If there will only ever be ONE value (and they state in their documentation that yes indeed there will only ever be ONE value), WHY the heck would you make the return object an array instead of just passing back to the developer the value of the primitive type itself directly??? That's just plain bad API construction right there, forcing the developers to use SearchColumnField[0].searchValue every time rather than just SearchColumnField.searchValue!
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);