The business rules are simple. We have a method that takes a JObject as a parm. Convert it to a c# poco.
The json needs to represent a single object.
No arrays allowed. If you need to do it three times call the method three times.
So for example this would be valid json:
{
"CustomerId": 669616948,
"FirstName": "ERIC",
"LastName": "TEST2",
"BirthYear": 0,
"BirthMonth": 0,
"CustomerState": 0,
"LegalAddressState": null,
"Username": "ERIC2222"
}
this would not:
{
"Participants": [
{
"CustomerId": 669616948,
"FirstName": "ERIC",
"LastName": "TEST2",
"BirthYear": 0,
"BirthMonth": 0,
"CustomerState": 0,
"LegalAddressState": null,
"Username": "ERIC2222"
}
]
}
Currently this throws an exception when it tries to convert to a poco and while we can handle the exception I was looking for a way to detect if the JObject contained an array and gracefully exit.
So the json above is just a representation of what the JObject would look like but it IS a JObject.
The best I have been able to come up with is a measly string check.
JObject.ToString().Contains("[")
Any ideas on how to do an array check. If I could somehow get it to a JToken then I could do this (temp is of type JToken):
temp.Type == JTokenType.Array
TIA
As requested here is the conversion. payload is a JObject.
var customer = payload.ToObject<Customer>(_serializer);
What about this way?
dynamic value = jToken["Participants"];
if (value != null && value is JArray)
{
//gracefully exit.
}
You can always write a custom JsonConverter that walks the json tree (using a technique such as that described in this answer https://stackoverflow.com/a/19646950/1165998), checking both the type and the value type for JArray and returning null if it is:
public class ProhibitArraysConverter<T> : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
var jsonObject = JToken.Load(reader);
if (ContainsArray(jsonObject))
return null;
T target = (T)Activator.CreateInstance(objectType);
serializer.Populate(jsonObject.CreateReader(), target);
return target;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(T);
}
private static bool ContainsArray(JToken containerToken)
{
if (containerToken.Type == JTokenType.Object)
{
foreach (JProperty child in containerToken.Children<JProperty>())
{
if (child.Type == JTokenType.Array ||
child.Value.Type == JTokenType.Array)
{
return true;
}
ContainsArray(child.Value);
}
}
else if (containerToken.Type == JTokenType.Array)
{
return true;
}
return false;
}
}
This will return the deserialized data in your first example and null for your second.
Am not quite sure about your requirements. But this is one way to go:
[TestMethod]
public void DeserializeTest()
{
var jsonStr1 = "{\"CustomerId\": 669616948,\"FirstName\": \"ERIC\",\"LastName\": \"TEST2\",\"BirthYear\": 0,\"BirthMonth\": 0,\"CustomerState\": 0,\"LegalAddressState\": null,\"Username\": \"ERIC2222\"}";
JToken token1 = JToken.Parse(jsonStr1);
var participantsFromToken1 = token1["Participants"];
Console.WriteLine(participantsFromToken1 != null && participantsFromToken1.Type == JTokenType.Array
? "Hey, token1 is an array"
: "Hey, token1 is not an array");
var jsonStr2 =
"{\"Participants\": [{\"CustomerId\": 669616948,\"FirstName\": \"ERIC\",\"LastName\": \"TEST2\",\"BirthYear\": 0,\"BirthMonth\": 0,\"CustomerState\": 0,\"LegalAddressState\": null,\"Username\": \"ERIC2222\"}]}";
JToken token2 = JToken.Parse(jsonStr2);
var participantsFromToken2 = token2["Participants"];
Console.WriteLine(participantsFromToken2 != null && participantsFromToken2.Type == JTokenType.Array
? "Hey, token2 is an array"
: "Hey, token2 is not an array");
}
Related
I would like to skip some dictionary value that same as a default value.
Here is the simplified code of that dictionary
public Dictionary<int, Item> allItems;
public class Item
{
public bool IsSelected;
public List<string> SelectionInfo;
}
So, as of right now, my JSON output looks like this:
"allItems": {
"0": {
"IsSelected": true,
"SelectionInfo": [
"yes",
"maybe",
"no"
]
},
"1": {
"IsSelected": false,
"SelectionInfo": []
}
}
I want to skip the "1" But don't just entirely skip it, at least keep the key so it can be restored at later. and have 0 and 1 on the dictionary
Like this?
"allItems": {
"0": {
"IsSelected": true,
"SelectionInfo": [
"yes",
"maybe",
"no"
]
},
"1": { }
}
I was looking around and found out that you can use JsonConverter. But my case JSON tool is on the other Project (Utility.Please.TurnToJson(item);) and I want to keep it consistent and use only one JSON across all projects. Maybe if JsonConverter is the only option, at least provide me the solution on how to pass the custom JsonConverter to that project.
///somewhat like this?
Utility.Please.TurnToJson(item, new List<object>(){
typeof(Custom1),
typeof(Custom2)
});
Here is the JsonConverter that can give you your expected output.
The converter can keep your key ai it is if IsSelected = false and SelectionInfo array is empty.
public class MyCustomJsonConverter : JsonConverter
{
public override bool CanRead
{
get { return false; }
}
public override bool CanConvert(Type objectType)
{
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException("Unnecessary because CanRead is false. The type will skip the converter.");
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JObject jObj = new JObject();
if (value != null)
{
var dict = JObject.Parse(value as string)["allItems"].ToObject<Dictionary<string, Item>>();
foreach (var item in dict)
{
JObject jObject = new JObject();
if (item.Value.IsSelected == false && item.Value.SelectionInfo.Count == 0)
{
jObj.Add(new JProperty(item.Key, new JObject()));
}
else
{
jObj.Add(new JProperty(item.Key, JObject.FromObject(item.Value)));
}
}
}
JObject jMainObject = new JObject();
jMainObject.Add(new JProperty("allItems", jObj));
jMainObject.WriteTo(writer);
}
}
Usage:
string json = File.ReadAllText(#"Path to your json file");
string output = JsonConvert.SerializeObject(json, new MyCustomJsonConverter());
Output:
There is some code (which I can't change) that uses Newtonsoft.Json's DeserializeObject<T>(strJSONData) to take data from a web request and convert it to a class object (I can change the class). By decorating my class properties with [DataMember(Name = "raw_property_name")] I can map the raw JSON data to the correct property in my class. Is there a way I can map the child property of a JSON complex object to a simple property? Here's an example:
{
"picture":
{
"id": 123456,
"data":
{
"type": "jpg",
"url": "http://www.someplace.com/mypicture.jpg"
}
}
}
I don't care about any of the rest of the picture object except for URL, and so don't want to setup a complex object in my C# class. I really just want something like:
[DataMember(Name = "picture.data.url")]
public string ProfilePicture { get; set; }
Is this possible?
Well, if you just need a single extra property, one simple approach is to parse your JSON to a JObject, use ToObject() to populate your class from the JObject, and then use SelectToken() to pull in the extra property.
So, assuming your class looked something like this:
class Person
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("age")]
public string Age { get; set; }
public string ProfilePicture { get; set; }
}
You could do this:
string json = #"
{
""name"" : ""Joe Shmoe"",
""age"" : 26,
""picture"":
{
""id"": 123456,
""data"":
{
""type"": ""jpg"",
""url"": ""http://www.someplace.com/mypicture.jpg""
}
}
}";
JObject jo = JObject.Parse(json);
Person p = jo.ToObject<Person>();
p.ProfilePicture = (string)jo.SelectToken("picture.data.url");
Fiddle: https://dotnetfiddle.net/7gnJCK
If you prefer a more fancy solution, you could make a custom JsonConverter to enable the JsonProperty attribute to behave like you describe. The converter would need to operate at the class level and use some reflection combined with the above technique to populate all the properties. Here is what it might look like in code:
class JsonPathConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
object targetObj = Activator.CreateInstance(objectType);
foreach (PropertyInfo prop in objectType.GetProperties()
.Where(p => p.CanRead && p.CanWrite))
{
JsonPropertyAttribute att = prop.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
string jsonPath = (att != null ? att.PropertyName : prop.Name);
JToken token = jo.SelectToken(jsonPath);
if (token != null && token.Type != JTokenType.Null)
{
object value = token.ToObject(prop.PropertyType, serializer);
prop.SetValue(targetObj, value, null);
}
}
return targetObj;
}
public override bool CanConvert(Type objectType)
{
// CanConvert is not called when [JsonConverter] attribute is used
return false;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
To demonstrate, let's assume the JSON now looks like the following:
{
"name": "Joe Shmoe",
"age": 26,
"picture": {
"id": 123456,
"data": {
"type": "jpg",
"url": "http://www.someplace.com/mypicture.jpg"
}
},
"favorites": {
"movie": {
"title": "The Godfather",
"starring": "Marlon Brando",
"year": 1972
},
"color": "purple"
}
}
...and you are interested in the person's favorite movie (title and year) and favorite color in addition to the information from before. You would first mark your target class with a [JsonConverter] attribute to associate it with the custom converter, then use [JsonProperty] attributes on each property, specifying the desired property path (case sensitive) as the name. The target properties don't have to be primitives either-- you can use a child class like I did here with Movie (and notice there's no intervening Favorites class required).
[JsonConverter(typeof(JsonPathConverter))]
class Person
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("age")]
public int Age { get; set; }
[JsonProperty("picture.data.url")]
public string ProfilePicture { get; set; }
[JsonProperty("favorites.movie")]
public Movie FavoriteMovie { get; set; }
[JsonProperty("favorites.color")]
public string FavoriteColor { get; set; }
}
// Don't need to mark up these properties because they are covered by the
// property paths in the Person class
class Movie
{
public string Title { get; set; }
public int Year { get; set; }
}
With all the attributes in place, you can just deserialize as normal and it should "just work":
Person p = JsonConvert.DeserializeObject<Person>(json);
Fiddle: https://dotnetfiddle.net/Ljw32O
The marked answer is not 100% complete as it ignores any IContractResolver that may be registered such as CamelCasePropertyNamesContractResolver etc.
Also returning false for can convert will prevent other user cases so i changed it to return objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().Any();
Here is the updated version:
https://dotnetfiddle.net/F8C8U8
I also removed the need to set a JsonProperty on a property as illustrated in the link.
If for some reason the link above dies or explodes i also including the code below:
public class JsonPathConverter : JsonConverter
{
/// <inheritdoc />
public override object ReadJson(
JsonReader reader,
Type objectType,
object existingValue,
JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
object targetObj = Activator.CreateInstance(objectType);
foreach (PropertyInfo prop in objectType.GetProperties().Where(p => p.CanRead && p.CanWrite))
{
JsonPropertyAttribute att = prop.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
string jsonPath = att != null ? att.PropertyName : prop.Name;
if (serializer.ContractResolver is DefaultContractResolver)
{
var resolver = (DefaultContractResolver)serializer.ContractResolver;
jsonPath = resolver.GetResolvedPropertyName(jsonPath);
}
if (!Regex.IsMatch(jsonPath, #"^[a-zA-Z0-9_.-]+$"))
{
throw new InvalidOperationException($"JProperties of JsonPathConverter can have only letters, numbers, underscores, hiffens and dots but name was ${jsonPath}."); // Array operations not permitted
}
JToken token = jo.SelectToken(jsonPath);
if (token != null && token.Type != JTokenType.Null)
{
object value = token.ToObject(prop.PropertyType, serializer);
prop.SetValue(targetObj, value, null);
}
}
return targetObj;
}
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
// CanConvert is not called when [JsonConverter] attribute is used
return objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().Any();
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var properties = value.GetType().GetRuntimeProperties().Where(p => p.CanRead && p.CanWrite);
JObject main = new JObject();
foreach (PropertyInfo prop in properties)
{
JsonPropertyAttribute att = prop.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
string jsonPath = att != null ? att.PropertyName : prop.Name;
if (serializer.ContractResolver is DefaultContractResolver)
{
var resolver = (DefaultContractResolver)serializer.ContractResolver;
jsonPath = resolver.GetResolvedPropertyName(jsonPath);
}
var nesting = jsonPath.Split('.');
JObject lastLevel = main;
for (int i = 0; i < nesting.Length; i++)
{
if (i == nesting.Length - 1)
{
lastLevel[nesting[i]] = new JValue(prop.GetValue(value));
}
else
{
if (lastLevel[nesting[i]] == null)
{
lastLevel[nesting[i]] = new JObject();
}
lastLevel = (JObject)lastLevel[nesting[i]];
}
}
}
serializer.Serialize(writer, main);
}
}
Istead of doing
lastLevel [nesting [i]] = new JValue(prop.GetValue (value));
You have to do
lastLevel[nesting[i]] = JValue.FromObject(jValue);
Otherwise we have a
Could not determine JSON object type for type ...
exception
A complete piece of code would be this:
object jValue = prop.GetValue(value);
if (prop.PropertyType.IsArray)
{
if(jValue != null)
//https://stackoverflow.com/a/20769644/249895
lastLevel[nesting[i]] = JArray.FromObject(jValue);
}
else
{
if (prop.PropertyType.IsClass && prop.PropertyType != typeof(System.String))
{
if (jValue != null)
lastLevel[nesting[i]] = JValue.FromObject(jValue);
}
else
{
lastLevel[nesting[i]] = new JValue(jValue);
}
}
If someone needs to use the JsonPathConverter of #BrianRogers also with the WriteJson option, here's a solution (that works only for paths with dots only):
Remove the CanWrite property so that it becomes true by default again.
Replace WriteJson code by the following:
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
var properties = value.GetType().GetRuntimeProperties ().Where(p => p.CanRead && p.CanWrite);
JObject main = new JObject ();
foreach (PropertyInfo prop in properties) {
JsonPropertyAttribute att = prop.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
string jsonPath = (att != null ? att.PropertyName : prop.Name);
var nesting=jsonPath.Split(new[] { '.' });
JObject lastLevel = main;
for (int i = 0; i < nesting.Length; i++) {
if (i == nesting.Length - 1) {
lastLevel [nesting [i]] = new JValue(prop.GetValue (value));
} else {
if (lastLevel [nesting [i]] == null) {
lastLevel [nesting [i]] = new JObject ();
}
lastLevel = (JObject)lastLevel [nesting [i]];
}
}
}
serializer.Serialize (writer, main);
}
As I said above, this only works for paths that contains dots. Given that, you should add the following code to ReadJson in order to prevent other cases:
[...]
string jsonPath = (att != null ? att.PropertyName : prop.Name);
if (!Regex.IsMatch(jsonPath, #"^[a-zA-Z0-9_.-]+$")) {
throw new InvalidOperationException("JProperties of JsonPathConverter can have only letters, numbers, underscores, hiffens and dots."); //Array operations not permitted
}
JToken token = jo.SelectToken(jsonPath);
[...]
Another solution (original source code was taken from https://gist.github.com/lucd/cdd57a2602bd975ec0a6). I've cleaned source codes and added classes / arrays of classes support. Requires C# 7
/// <summary>
/// Custom converter that allows mapping a JSON value according to a navigation path.
/// </summary>
/// <typeparam name="T">Class which contains nested properties.</typeparam>
public class NestedJsonConverter<T> : JsonConverter
where T : new()
{
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(T);
}
/// <inheritdoc />
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var result = new T();
var data = JObject.Load(reader);
// Get all properties of a provided class
var properties = result
.GetType()
.GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance);
foreach (var propertyInfo in properties)
{
var jsonPropertyAttribute = propertyInfo
.GetCustomAttributes(false)
.FirstOrDefault(attribute => attribute is JsonPropertyAttribute);
// Use either custom JSON property or regular property name
var propertyName = jsonPropertyAttribute != null
? ((JsonPropertyAttribute)jsonPropertyAttribute).PropertyName
: propertyInfo.Name;
if (string.IsNullOrEmpty(propertyName))
{
continue;
}
// Split by the delimiter, and traverse recursively according to the path
var names = propertyName.Split('/');
object propertyValue = null;
JToken token = null;
for (int i = 0; i < names.Length; i++)
{
var name = names[i];
var isLast = i == names.Length - 1;
token = token == null
? data.GetValue(name, StringComparison.OrdinalIgnoreCase)
: ((JObject)token).GetValue(name, StringComparison.OrdinalIgnoreCase);
if (token == null)
{
// Silent fail: exit the loop if the specified path was not found
break;
}
if (token is JValue || token is JArray || (token is JObject && isLast))
{
// simple value / array of items / complex object (only if the last chain)
propertyValue = token.ToObject(propertyInfo.PropertyType, serializer);
}
}
if (propertyValue == null)
{
continue;
}
propertyInfo.SetValue(result, propertyValue);
}
return result;
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
}
}
Sample model
public class SomeModel
{
public List<string> Records { get; set; }
[JsonProperty("level1/level2/level3")]
public string SomeValue{ get; set; }
}
sample json:
{
"records": ["some value1", "somevalue 2"],
"level1":
{
"level2":
{
"level3": "gotcha!"
}
}
}
Once you have added a JsonConverter, you can use it like this:
var json = "{}"; // input json string
var settings = new JsonSerializerSettings();
settings.Converters.Add(new NestedJsonConverter<SomeModel>());
var result = JsonConvert.DeserializeObject<SomeModel>(json , settings);
Fiddle: https://dotnetfiddle.net/pBK9dj
Keep mind that if you have several nested properties in different classes then you would need to add as many converters as many classes you have:
settings.Converters.Add(new NestedJsonConverter<Model1>());
settings.Converters.Add(new NestedJsonConverter<Model2>());
...
FYI, I added a little extra to account for any other converts on the nested property. For example, we had a nested DateTime? property, but the result was sometimes provided as an empty string, so we had to have another JsonConverter which accommodated for this.
Our class ended up like this:
[JsonConverter(typeof(JsonPathConverter))] // Reference the nesting class
public class Timesheet {
[JsonConverter(typeof(InvalidDateConverter))]
[JsonProperty("time.start")]
public DateTime? StartTime { get; set; }
}
The JSON was:
{
time: {
start: " "
}
}
The final update to the JsonConverter above is:
var token = jo.SelectToken(jsonPath);
if (token != null && token.Type != JTokenType.Null)
{
object value = null;
// Apply custom converters
var converters = prop.GetCustomAttributes<JsonConverterAttribute>(); //(true).OfType<JsonPropertyAttribute>().FirstOrDefault();
if (converters != null && converters.Any())
{
foreach (var converter in converters)
{
var converterType = (JsonConverter)Activator.CreateInstance(converter.ConverterType);
if (!converterType.CanRead) continue;
value = converterType.ReadJson(token.CreateReader(), prop.PropertyType, value, serializer);
}
}
else
{
value = token.ToObject(prop.PropertyType, serializer);
}
prop.SetValue(targetObj, value, null);
}
With help of all the answers in this thread I came up with solution of JsonPathConverter class (used as JsonConverter attribute) which implements both ReadJson and WriteJson that works with forward slashes.
The class implementation:
/// <summary>
/// Custom converter that allows mapping a JSON value according to a navigation path using forward slashes "/".
/// </summary>
public class JsonPathConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject data = JObject.Load(reader);
object resultObject = Activator.CreateInstance(objectType);
// Get all properties of a provided class
PropertyInfo[] properties = objectType
.GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance);
foreach (PropertyInfo propertyInfo in properties)
{
JsonPropertyAttribute propertyAttribute = propertyInfo
.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
// Use either custom JSON property or regular property name
string propertyJsonPath = propertyAttribute != null
? propertyAttribute.PropertyName
: propertyInfo.Name;
if (string.IsNullOrEmpty(propertyJsonPath))
{
continue;
}
// Split by the delimiter, and traverse recursively according to the path
string[] nesting = propertyJsonPath.Split('/');
object propertyValue = null;
JToken token = null;
for (int i = 0; i < nesting.Length; i++)
{
string name = nesting[i];
bool isLast = i == nesting.Length - 1;
token = token == null
? data.GetValue(name, StringComparison.OrdinalIgnoreCase)
: ((JObject)token).GetValue(name, StringComparison.OrdinalIgnoreCase);
if (token == null)
{
// Silent fail: exit the loop if the specified path was not found
break;
}
if (token is JValue || token is JArray || (token is JObject && isLast))
{
// simple value / array of items / complex object (only if the last chain)
propertyValue = token.ToObject(propertyInfo.PropertyType, serializer);
}
}
if (propertyValue == null)
{
continue;
}
propertyInfo.SetValue(resultObject, propertyValue);
}
return resultObject;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JObject resultJson = new();
// Get all properties of a provided class
IEnumerable<PropertyInfo> properties = value
.GetType().GetRuntimeProperties().Where(p => p.CanRead && p.CanWrite);
foreach (PropertyInfo propertyInfo in properties)
{
JsonPropertyAttribute propertyAttribute = propertyInfo
.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
// Use either custom JSON property or regular property name
string propertyJsonPath = propertyAttribute != null
? propertyAttribute.PropertyName
: propertyInfo.Name;
if (serializer.ContractResolver is DefaultContractResolver resolver)
{
propertyJsonPath = resolver.GetResolvedPropertyName(propertyJsonPath);
}
if (string.IsNullOrEmpty(propertyJsonPath))
{
continue;
}
// Split by the delimiter, and traverse according to the path
string[] nesting = propertyJsonPath.Split('/');
JObject lastJsonLevel = resultJson;
for (int i = 0; i < nesting.Length; i++)
{
if (i == nesting.Length - 1)
{
lastJsonLevel[nesting[i]] = JToken.FromObject(propertyInfo.GetValue(value));
}
else
{
if (lastJsonLevel[nesting[i]] == null)
{
lastJsonLevel[nesting[i]] = new JObject();
}
lastJsonLevel = (JObject)lastJsonLevel[nesting[i]];
}
}
}
serializer.Serialize(writer, resultJson);
}
public override bool CanConvert(Type objectType)
{
return objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().Any();
}
}
Please keep in mind you will also need these usings:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System.Reflection;
Usage of this custom JsonConverter is very simple. Let's say we have the OP's JSON:
{
"picture":
{
"id": 123456,
"data":
{
"type": "jpg",
"url": "http://www.someplace.com/mypicture.jpg"
}
}
}
According to that, we can create object that will hold the JSON data:
[JsonConverter(typeof(JsonPathConverter))]
public class Picture
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("data/type")]
public int Type { get; set; }
[JsonProperty("data/url")]
public string Url { get; set; }
}
NOTE: Don't forget to mark your target class with a JsonConverter attribute and specify the newly created JsonPathConverter converter as shown above.
Then just deserialize the JSON to our object as normal:
var picture = JsonConvert.DeserializeObject<Picture>(json);
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>.
There is some code (which I can't change) that uses Newtonsoft.Json's DeserializeObject<T>(strJSONData) to take data from a web request and convert it to a class object (I can change the class). By decorating my class properties with [DataMember(Name = "raw_property_name")] I can map the raw JSON data to the correct property in my class. Is there a way I can map the child property of a JSON complex object to a simple property? Here's an example:
{
"picture":
{
"id": 123456,
"data":
{
"type": "jpg",
"url": "http://www.someplace.com/mypicture.jpg"
}
}
}
I don't care about any of the rest of the picture object except for URL, and so don't want to setup a complex object in my C# class. I really just want something like:
[DataMember(Name = "picture.data.url")]
public string ProfilePicture { get; set; }
Is this possible?
Well, if you just need a single extra property, one simple approach is to parse your JSON to a JObject, use ToObject() to populate your class from the JObject, and then use SelectToken() to pull in the extra property.
So, assuming your class looked something like this:
class Person
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("age")]
public string Age { get; set; }
public string ProfilePicture { get; set; }
}
You could do this:
string json = #"
{
""name"" : ""Joe Shmoe"",
""age"" : 26,
""picture"":
{
""id"": 123456,
""data"":
{
""type"": ""jpg"",
""url"": ""http://www.someplace.com/mypicture.jpg""
}
}
}";
JObject jo = JObject.Parse(json);
Person p = jo.ToObject<Person>();
p.ProfilePicture = (string)jo.SelectToken("picture.data.url");
Fiddle: https://dotnetfiddle.net/7gnJCK
If you prefer a more fancy solution, you could make a custom JsonConverter to enable the JsonProperty attribute to behave like you describe. The converter would need to operate at the class level and use some reflection combined with the above technique to populate all the properties. Here is what it might look like in code:
class JsonPathConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
object targetObj = Activator.CreateInstance(objectType);
foreach (PropertyInfo prop in objectType.GetProperties()
.Where(p => p.CanRead && p.CanWrite))
{
JsonPropertyAttribute att = prop.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
string jsonPath = (att != null ? att.PropertyName : prop.Name);
JToken token = jo.SelectToken(jsonPath);
if (token != null && token.Type != JTokenType.Null)
{
object value = token.ToObject(prop.PropertyType, serializer);
prop.SetValue(targetObj, value, null);
}
}
return targetObj;
}
public override bool CanConvert(Type objectType)
{
// CanConvert is not called when [JsonConverter] attribute is used
return false;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
To demonstrate, let's assume the JSON now looks like the following:
{
"name": "Joe Shmoe",
"age": 26,
"picture": {
"id": 123456,
"data": {
"type": "jpg",
"url": "http://www.someplace.com/mypicture.jpg"
}
},
"favorites": {
"movie": {
"title": "The Godfather",
"starring": "Marlon Brando",
"year": 1972
},
"color": "purple"
}
}
...and you are interested in the person's favorite movie (title and year) and favorite color in addition to the information from before. You would first mark your target class with a [JsonConverter] attribute to associate it with the custom converter, then use [JsonProperty] attributes on each property, specifying the desired property path (case sensitive) as the name. The target properties don't have to be primitives either-- you can use a child class like I did here with Movie (and notice there's no intervening Favorites class required).
[JsonConverter(typeof(JsonPathConverter))]
class Person
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("age")]
public int Age { get; set; }
[JsonProperty("picture.data.url")]
public string ProfilePicture { get; set; }
[JsonProperty("favorites.movie")]
public Movie FavoriteMovie { get; set; }
[JsonProperty("favorites.color")]
public string FavoriteColor { get; set; }
}
// Don't need to mark up these properties because they are covered by the
// property paths in the Person class
class Movie
{
public string Title { get; set; }
public int Year { get; set; }
}
With all the attributes in place, you can just deserialize as normal and it should "just work":
Person p = JsonConvert.DeserializeObject<Person>(json);
Fiddle: https://dotnetfiddle.net/Ljw32O
The marked answer is not 100% complete as it ignores any IContractResolver that may be registered such as CamelCasePropertyNamesContractResolver etc.
Also returning false for can convert will prevent other user cases so i changed it to return objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().Any();
Here is the updated version:
https://dotnetfiddle.net/F8C8U8
I also removed the need to set a JsonProperty on a property as illustrated in the link.
If for some reason the link above dies or explodes i also including the code below:
public class JsonPathConverter : JsonConverter
{
/// <inheritdoc />
public override object ReadJson(
JsonReader reader,
Type objectType,
object existingValue,
JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
object targetObj = Activator.CreateInstance(objectType);
foreach (PropertyInfo prop in objectType.GetProperties().Where(p => p.CanRead && p.CanWrite))
{
JsonPropertyAttribute att = prop.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
string jsonPath = att != null ? att.PropertyName : prop.Name;
if (serializer.ContractResolver is DefaultContractResolver)
{
var resolver = (DefaultContractResolver)serializer.ContractResolver;
jsonPath = resolver.GetResolvedPropertyName(jsonPath);
}
if (!Regex.IsMatch(jsonPath, #"^[a-zA-Z0-9_.-]+$"))
{
throw new InvalidOperationException($"JProperties of JsonPathConverter can have only letters, numbers, underscores, hiffens and dots but name was ${jsonPath}."); // Array operations not permitted
}
JToken token = jo.SelectToken(jsonPath);
if (token != null && token.Type != JTokenType.Null)
{
object value = token.ToObject(prop.PropertyType, serializer);
prop.SetValue(targetObj, value, null);
}
}
return targetObj;
}
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
// CanConvert is not called when [JsonConverter] attribute is used
return objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().Any();
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var properties = value.GetType().GetRuntimeProperties().Where(p => p.CanRead && p.CanWrite);
JObject main = new JObject();
foreach (PropertyInfo prop in properties)
{
JsonPropertyAttribute att = prop.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
string jsonPath = att != null ? att.PropertyName : prop.Name;
if (serializer.ContractResolver is DefaultContractResolver)
{
var resolver = (DefaultContractResolver)serializer.ContractResolver;
jsonPath = resolver.GetResolvedPropertyName(jsonPath);
}
var nesting = jsonPath.Split('.');
JObject lastLevel = main;
for (int i = 0; i < nesting.Length; i++)
{
if (i == nesting.Length - 1)
{
lastLevel[nesting[i]] = new JValue(prop.GetValue(value));
}
else
{
if (lastLevel[nesting[i]] == null)
{
lastLevel[nesting[i]] = new JObject();
}
lastLevel = (JObject)lastLevel[nesting[i]];
}
}
}
serializer.Serialize(writer, main);
}
}
Istead of doing
lastLevel [nesting [i]] = new JValue(prop.GetValue (value));
You have to do
lastLevel[nesting[i]] = JValue.FromObject(jValue);
Otherwise we have a
Could not determine JSON object type for type ...
exception
A complete piece of code would be this:
object jValue = prop.GetValue(value);
if (prop.PropertyType.IsArray)
{
if(jValue != null)
//https://stackoverflow.com/a/20769644/249895
lastLevel[nesting[i]] = JArray.FromObject(jValue);
}
else
{
if (prop.PropertyType.IsClass && prop.PropertyType != typeof(System.String))
{
if (jValue != null)
lastLevel[nesting[i]] = JValue.FromObject(jValue);
}
else
{
lastLevel[nesting[i]] = new JValue(jValue);
}
}
If someone needs to use the JsonPathConverter of #BrianRogers also with the WriteJson option, here's a solution (that works only for paths with dots only):
Remove the CanWrite property so that it becomes true by default again.
Replace WriteJson code by the following:
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
var properties = value.GetType().GetRuntimeProperties ().Where(p => p.CanRead && p.CanWrite);
JObject main = new JObject ();
foreach (PropertyInfo prop in properties) {
JsonPropertyAttribute att = prop.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
string jsonPath = (att != null ? att.PropertyName : prop.Name);
var nesting=jsonPath.Split(new[] { '.' });
JObject lastLevel = main;
for (int i = 0; i < nesting.Length; i++) {
if (i == nesting.Length - 1) {
lastLevel [nesting [i]] = new JValue(prop.GetValue (value));
} else {
if (lastLevel [nesting [i]] == null) {
lastLevel [nesting [i]] = new JObject ();
}
lastLevel = (JObject)lastLevel [nesting [i]];
}
}
}
serializer.Serialize (writer, main);
}
As I said above, this only works for paths that contains dots. Given that, you should add the following code to ReadJson in order to prevent other cases:
[...]
string jsonPath = (att != null ? att.PropertyName : prop.Name);
if (!Regex.IsMatch(jsonPath, #"^[a-zA-Z0-9_.-]+$")) {
throw new InvalidOperationException("JProperties of JsonPathConverter can have only letters, numbers, underscores, hiffens and dots."); //Array operations not permitted
}
JToken token = jo.SelectToken(jsonPath);
[...]
Another solution (original source code was taken from https://gist.github.com/lucd/cdd57a2602bd975ec0a6). I've cleaned source codes and added classes / arrays of classes support. Requires C# 7
/// <summary>
/// Custom converter that allows mapping a JSON value according to a navigation path.
/// </summary>
/// <typeparam name="T">Class which contains nested properties.</typeparam>
public class NestedJsonConverter<T> : JsonConverter
where T : new()
{
/// <inheritdoc />
public override bool CanConvert(Type objectType)
{
return objectType == typeof(T);
}
/// <inheritdoc />
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var result = new T();
var data = JObject.Load(reader);
// Get all properties of a provided class
var properties = result
.GetType()
.GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance);
foreach (var propertyInfo in properties)
{
var jsonPropertyAttribute = propertyInfo
.GetCustomAttributes(false)
.FirstOrDefault(attribute => attribute is JsonPropertyAttribute);
// Use either custom JSON property or regular property name
var propertyName = jsonPropertyAttribute != null
? ((JsonPropertyAttribute)jsonPropertyAttribute).PropertyName
: propertyInfo.Name;
if (string.IsNullOrEmpty(propertyName))
{
continue;
}
// Split by the delimiter, and traverse recursively according to the path
var names = propertyName.Split('/');
object propertyValue = null;
JToken token = null;
for (int i = 0; i < names.Length; i++)
{
var name = names[i];
var isLast = i == names.Length - 1;
token = token == null
? data.GetValue(name, StringComparison.OrdinalIgnoreCase)
: ((JObject)token).GetValue(name, StringComparison.OrdinalIgnoreCase);
if (token == null)
{
// Silent fail: exit the loop if the specified path was not found
break;
}
if (token is JValue || token is JArray || (token is JObject && isLast))
{
// simple value / array of items / complex object (only if the last chain)
propertyValue = token.ToObject(propertyInfo.PropertyType, serializer);
}
}
if (propertyValue == null)
{
continue;
}
propertyInfo.SetValue(result, propertyValue);
}
return result;
}
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
}
}
Sample model
public class SomeModel
{
public List<string> Records { get; set; }
[JsonProperty("level1/level2/level3")]
public string SomeValue{ get; set; }
}
sample json:
{
"records": ["some value1", "somevalue 2"],
"level1":
{
"level2":
{
"level3": "gotcha!"
}
}
}
Once you have added a JsonConverter, you can use it like this:
var json = "{}"; // input json string
var settings = new JsonSerializerSettings();
settings.Converters.Add(new NestedJsonConverter<SomeModel>());
var result = JsonConvert.DeserializeObject<SomeModel>(json , settings);
Fiddle: https://dotnetfiddle.net/pBK9dj
Keep mind that if you have several nested properties in different classes then you would need to add as many converters as many classes you have:
settings.Converters.Add(new NestedJsonConverter<Model1>());
settings.Converters.Add(new NestedJsonConverter<Model2>());
...
FYI, I added a little extra to account for any other converts on the nested property. For example, we had a nested DateTime? property, but the result was sometimes provided as an empty string, so we had to have another JsonConverter which accommodated for this.
Our class ended up like this:
[JsonConverter(typeof(JsonPathConverter))] // Reference the nesting class
public class Timesheet {
[JsonConverter(typeof(InvalidDateConverter))]
[JsonProperty("time.start")]
public DateTime? StartTime { get; set; }
}
The JSON was:
{
time: {
start: " "
}
}
The final update to the JsonConverter above is:
var token = jo.SelectToken(jsonPath);
if (token != null && token.Type != JTokenType.Null)
{
object value = null;
// Apply custom converters
var converters = prop.GetCustomAttributes<JsonConverterAttribute>(); //(true).OfType<JsonPropertyAttribute>().FirstOrDefault();
if (converters != null && converters.Any())
{
foreach (var converter in converters)
{
var converterType = (JsonConverter)Activator.CreateInstance(converter.ConverterType);
if (!converterType.CanRead) continue;
value = converterType.ReadJson(token.CreateReader(), prop.PropertyType, value, serializer);
}
}
else
{
value = token.ToObject(prop.PropertyType, serializer);
}
prop.SetValue(targetObj, value, null);
}
With help of all the answers in this thread I came up with solution of JsonPathConverter class (used as JsonConverter attribute) which implements both ReadJson and WriteJson that works with forward slashes.
The class implementation:
/// <summary>
/// Custom converter that allows mapping a JSON value according to a navigation path using forward slashes "/".
/// </summary>
public class JsonPathConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject data = JObject.Load(reader);
object resultObject = Activator.CreateInstance(objectType);
// Get all properties of a provided class
PropertyInfo[] properties = objectType
.GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance);
foreach (PropertyInfo propertyInfo in properties)
{
JsonPropertyAttribute propertyAttribute = propertyInfo
.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
// Use either custom JSON property or regular property name
string propertyJsonPath = propertyAttribute != null
? propertyAttribute.PropertyName
: propertyInfo.Name;
if (string.IsNullOrEmpty(propertyJsonPath))
{
continue;
}
// Split by the delimiter, and traverse recursively according to the path
string[] nesting = propertyJsonPath.Split('/');
object propertyValue = null;
JToken token = null;
for (int i = 0; i < nesting.Length; i++)
{
string name = nesting[i];
bool isLast = i == nesting.Length - 1;
token = token == null
? data.GetValue(name, StringComparison.OrdinalIgnoreCase)
: ((JObject)token).GetValue(name, StringComparison.OrdinalIgnoreCase);
if (token == null)
{
// Silent fail: exit the loop if the specified path was not found
break;
}
if (token is JValue || token is JArray || (token is JObject && isLast))
{
// simple value / array of items / complex object (only if the last chain)
propertyValue = token.ToObject(propertyInfo.PropertyType, serializer);
}
}
if (propertyValue == null)
{
continue;
}
propertyInfo.SetValue(resultObject, propertyValue);
}
return resultObject;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JObject resultJson = new();
// Get all properties of a provided class
IEnumerable<PropertyInfo> properties = value
.GetType().GetRuntimeProperties().Where(p => p.CanRead && p.CanWrite);
foreach (PropertyInfo propertyInfo in properties)
{
JsonPropertyAttribute propertyAttribute = propertyInfo
.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.FirstOrDefault();
// Use either custom JSON property or regular property name
string propertyJsonPath = propertyAttribute != null
? propertyAttribute.PropertyName
: propertyInfo.Name;
if (serializer.ContractResolver is DefaultContractResolver resolver)
{
propertyJsonPath = resolver.GetResolvedPropertyName(propertyJsonPath);
}
if (string.IsNullOrEmpty(propertyJsonPath))
{
continue;
}
// Split by the delimiter, and traverse according to the path
string[] nesting = propertyJsonPath.Split('/');
JObject lastJsonLevel = resultJson;
for (int i = 0; i < nesting.Length; i++)
{
if (i == nesting.Length - 1)
{
lastJsonLevel[nesting[i]] = JToken.FromObject(propertyInfo.GetValue(value));
}
else
{
if (lastJsonLevel[nesting[i]] == null)
{
lastJsonLevel[nesting[i]] = new JObject();
}
lastJsonLevel = (JObject)lastJsonLevel[nesting[i]];
}
}
}
serializer.Serialize(writer, resultJson);
}
public override bool CanConvert(Type objectType)
{
return objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().Any();
}
}
Please keep in mind you will also need these usings:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using System.Reflection;
Usage of this custom JsonConverter is very simple. Let's say we have the OP's JSON:
{
"picture":
{
"id": 123456,
"data":
{
"type": "jpg",
"url": "http://www.someplace.com/mypicture.jpg"
}
}
}
According to that, we can create object that will hold the JSON data:
[JsonConverter(typeof(JsonPathConverter))]
public class Picture
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("data/type")]
public int Type { get; set; }
[JsonProperty("data/url")]
public string Url { get; set; }
}
NOTE: Don't forget to mark your target class with a JsonConverter attribute and specify the newly created JsonPathConverter converter as shown above.
Then just deserialize the JSON to our object as normal:
var picture = JsonConvert.DeserializeObject<Picture>(json);
This question already has answers here:
How to handle both a single item and an array for the same property using JSON.net
(9 answers)
Closed 7 years ago.
I need to consume JSON from a REST service. For one of the fields, the service sometimes returns a string for the value yet other times returns an array of strings for the value.
For example:
var jsonArrayResponse = "{ \"id\" : \"abc\", \"relatedIds\" : [ \"def\", \"ghi\", \"jkl\" ] }";
var jsonSingleResponse = "{ \"id\" : \"123\", \"relatedIds\" : \"456\" }";
I have implemented the destination class using the SingleValueArrayConverter pattern found on this post. Here is the full set of code to illustrate my problem:
class Program
{
static void Main(string[] args)
{
// this works fine with the SingleValueArrayConverter
var jsonArray = "{ \"id\" : \"abc\", \"relatedIds\" : [ \"def\", \"ghi\", \"jkl\" ] }";
var objArray = (MyObject)JsonConvert.DeserializeObject(jsonArray, typeof(MyObject));
// this fails to parse "456" with Exception message:
// "Unable to cast object of type 'System.Object' to
// type 'System.Collections.Generic.List`1[System.String]'."
var jsonSingle = "{ \"id\" : \"123\", \"relatedIds\" : \"456\" }";
var objSingle = (MyObject)JsonConvert.DeserializeObject(jsonSingle, typeof (MyObject));
}
}
[DataContract]
public class MyObject
{
[DataMember(Name = "id")]
public string Id;
[DataMember(Name = "relatedIds")]
[JsonConverter(typeof(SingleValueArrayConverter<string>))]
public List<string> RelatedIds;
}
public class SingleValueArrayConverter<T> : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
object retVal = new Object();
if (reader.TokenType == JsonToken.StartObject)
{
T instance = (T)serializer.Deserialize(reader, typeof(T));
retVal = new List<T>() { instance };
}
else if (reader.TokenType == JsonToken.StartArray)
{
retVal = serializer.Deserialize(reader, objectType);
}
return retVal;
}
public override bool CanConvert(Type objectType)
{
return false;
}
}
How can I fix this to properly parse the two different JSON blobs into the same class?
UPDATE:
I modified the SingleValueArrayConverter to check the string case (as mentioned below), and things started working. I use the converter for more than just strings, so I had to add the first if statement, rather than modify it as suggested).
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
object retVal = new Object();
if (reader.TokenType == JsonToken.String)
{
string instance = (string)serializer.Deserialize(reader);
retVal = new List<string> () {instance};
}
else if (reader.TokenType == JsonToken.StartObject)
{
T instance = (T)serializer.Deserialize(reader, typeof(T));
retVal = new List<T>() { instance };
}
else if (reader.TokenType == JsonToken.StartArray)
{
retVal = serializer.Deserialize(reader, objectType);
}
return retVal;
}
public override bool CanConvert(Type objectType)
{
return false;
}
}
You could change this:
var jsonSingle = "{ \"id\" : \"123\", \"relatedIds\" : \"456\" }";
To this:
var jsonSingle = "{ \"id\" : \"123\", \"relatedIds\" : [ \"456\" ] }";
...and accept that relatedIds has one or more items therefore should always be a collection.
Alternatively you might make RelatedIds a generic:
public class MyObject<T>
{
// .... //
public T RelatedIds;
}
Which you could use like this:
var objArray = (MyObject<List<string>>)JsonConvert.DeserializeObject(jsonArray, typeof(MyObject<List<string>>));
var objSingle = (MyObject<object>)JsonConvert.DeserializeObject(jsonSingle, typeof (MyObject<object>));
Personally I prefer the previous option, for simplicity.
One thing you cannot do is make something defined as a string suddenly become a List<string> at runtime (actually maybe you could using dynamic, but better not to go there...). That is not allowed in a statically-typed language (though is possible in weakly typed languages such as JS or PHP).
Change JsonToken.StartObject to JsonToken.String. If you put a breakpoint right on the line object retVal = new Object(); you can see that reader.TokenType is JsonToken.String during the second instantiation. That's the only problem with your code.