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);
Related
I'm trying to fix my SendGridPlus library to deal with SendGrid events, but I'm having some trouble with the inconsistent treatment of categories in the API.
In the following example payload taken from the SendGrid API reference, you'll notice that the category property for each item can either be a single string or an array of strings.
[
{
"email": "john.doe#sendgrid.com",
"timestamp": 1337966815,
"category": [
"newuser",
"transactional"
],
"event": "open"
},
{
"email": "jane.doe#sendgrid.com",
"timestamp": 1337966815,
"category": "olduser",
"event": "open"
}
]
It seems my options to make JSON.NET like this are fixing the string before it comes in, or configuring JSON.NET to accept the incorrect data. I'd rather not do any string parsing if I can get away with it.
Is there any other way I can handle this using Json.Net?
The best way to handle this situation is to use a custom JsonConverter.
Before we get to the converter, we'll need to define a class to deserialize the data into. For the Categories property that can vary between a single item and an array, define it as a List<string> and mark it with a [JsonConverter] attribute so that JSON.Net will know to use the custom converter for that property. I would also recommend using [JsonProperty] attributes so that the member properties can be given meaningful names independent of what is defined in the JSON.
class Item
{
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("timestamp")]
public int Timestamp { get; set; }
[JsonProperty("event")]
public string Event { get; set; }
[JsonProperty("category")]
[JsonConverter(typeof(SingleOrArrayConverter<string>))]
public List<string> Categories { get; set; }
}
Here is how I would implement the converter. Notice I've made the converter generic so that it can be used with strings or other types of objects as needed.
class SingleOrArrayConverter<T> : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(List<T>));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
if (token.Type == JTokenType.Array)
{
return token.ToObject<List<T>>();
}
return new List<T> { token.ToObject<T>() };
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Here is an short program demonstrating the converter in action with your sample data:
class Program
{
static void Main(string[] args)
{
string json = #"
[
{
""email"": ""john.doe#sendgrid.com"",
""timestamp"": 1337966815,
""category"": [
""newuser"",
""transactional""
],
""event"": ""open""
},
{
""email"": ""jane.doe#sendgrid.com"",
""timestamp"": 1337966815,
""category"": ""olduser"",
""event"": ""open""
}
]";
List<Item> list = JsonConvert.DeserializeObject<List<Item>>(json);
foreach (Item obj in list)
{
Console.WriteLine("email: " + obj.Email);
Console.WriteLine("timestamp: " + obj.Timestamp);
Console.WriteLine("event: " + obj.Event);
Console.WriteLine("categories: " + string.Join(", ", obj.Categories));
Console.WriteLine();
}
}
}
And finally, here is the output of the above:
email: john.doe#sendgrid.com
timestamp: 1337966815
event: open
categories: newuser, transactional
email: jane.doe#sendgrid.com
timestamp: 1337966815
event: open
categories: olduser
Fiddle: https://dotnetfiddle.net/lERrmu
EDIT
If you need to go the other way, i.e. serialize, while keeping the same format, you can implement the WriteJson() method of the converter as shown below. (Be sure to remove the CanWrite override or change it to return true, or else WriteJson() will never be called.)
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
List<T> list = (List<T>)value;
if (list.Count == 1)
{
value = list[0];
}
serializer.Serialize(writer, value);
}
Fiddle: https://dotnetfiddle.net/XG3eRy
I was working on this for ages, and thanks to Brian for his answer.
All I am adding is the vb.net answer!:
Public Class SingleValueArrayConverter(Of T)
sometimes-array-and-sometimes-object
Inherits JsonConverter
Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
Throw New NotImplementedException()
End Sub
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim retVal As Object = New [Object]()
If reader.TokenType = JsonToken.StartObject Then
Dim instance As T = DirectCast(serializer.Deserialize(reader, GetType(T)), T)
retVal = New List(Of T)() From { _
instance _
}
ElseIf reader.TokenType = JsonToken.StartArray Then
retVal = serializer.Deserialize(reader, objectType)
End If
Return retVal
End Function
Public Overrides Function CanConvert(objectType As Type) As Boolean
Return False
End Function
End Class
then in your class:
<JsonProperty(PropertyName:="JsonName)> _
<JsonConverter(GetType(SingleValueArrayConverter(Of YourObject)))> _
Public Property YourLocalName As List(Of YourObject)
Hope this saves you some time
As a minor variation to the great answer by Brian Rogers, here are two tweaked versions of SingleOrArrayConverter<T>.
Firstly, here is a version that works for all List<T> for every type T that is not itself a collection:
public class SingleOrArrayListConverter : JsonConverter
{
// Adapted from this answer https://stackoverflow.com/a/18997172
// to https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n
// by Brian Rogers https://stackoverflow.com/users/10263/brian-rogers
readonly bool canWrite;
readonly IContractResolver resolver;
public SingleOrArrayListConverter() : this(false) { }
public SingleOrArrayListConverter(bool canWrite) : this(canWrite, null) { }
public SingleOrArrayListConverter(bool canWrite, IContractResolver resolver)
{
this.canWrite = canWrite;
// Use the global default resolver if none is passed in.
this.resolver = resolver ?? new JsonSerializer().ContractResolver;
}
static bool CanConvert(Type objectType, IContractResolver resolver)
{
Type itemType;
JsonArrayContract contract;
return CanConvert(objectType, resolver, out itemType, out contract);
}
static bool CanConvert(Type objectType, IContractResolver resolver, out Type itemType, out JsonArrayContract contract)
{
if ((itemType = objectType.GetListItemType()) == null)
{
itemType = null;
contract = null;
return false;
}
// Ensure that [JsonObject] is not applied to the type.
if ((contract = resolver.ResolveContract(objectType) as JsonArrayContract) == null)
return false;
var itemContract = resolver.ResolveContract(itemType);
// Not implemented for jagged arrays.
if (itemContract is JsonArrayContract)
return false;
return true;
}
public override bool CanConvert(Type objectType) { return CanConvert(objectType, resolver); }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
Type itemType;
JsonArrayContract contract;
if (!CanConvert(objectType, serializer.ContractResolver, out itemType, out contract))
throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), objectType));
if (reader.MoveToContent().TokenType == JsonToken.Null)
return null;
var list = (IList)(existingValue ?? contract.DefaultCreator());
if (reader.TokenType == JsonToken.StartArray)
serializer.Populate(reader, list);
else
// Here we take advantage of the fact that List<T> implements IList to avoid having to use reflection to call the generic Add<T> method.
list.Add(serializer.Deserialize(reader, itemType));
return list;
}
public override bool CanWrite { get { return canWrite; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var list = value as ICollection;
if (list == null)
throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), value.GetType()));
// Here we take advantage of the fact that List<T> implements IList to avoid having to use reflection to call the generic Count method.
if (list.Count == 1)
{
foreach (var item in list)
{
serializer.Serialize(writer, item);
break;
}
}
else
{
writer.WriteStartArray();
foreach (var item in list)
serializer.Serialize(writer, item);
writer.WriteEndArray();
}
}
}
public static partial class JsonExtensions
{
public static JsonReader MoveToContent(this JsonReader reader)
{
while ((reader.TokenType == JsonToken.Comment || reader.TokenType == JsonToken.None) && reader.Read())
;
return reader;
}
internal static Type GetListItemType(this Type type)
{
// Quick reject for performance
if (type.IsPrimitive || type.IsArray || type == typeof(string))
return null;
while (type != null)
{
if (type.IsGenericType)
{
var genType = type.GetGenericTypeDefinition();
if (genType == typeof(List<>))
return type.GetGenericArguments()[0];
}
type = type.BaseType;
}
return null;
}
}
It can be used as follows:
var settings = new JsonSerializerSettings
{
// Pass true if you want single-item lists to be reserialized as single items
Converters = { new SingleOrArrayListConverter(true) },
};
var list = JsonConvert.DeserializeObject<List<Item>>(json, settings);
Notes:
The converter avoids the need to pre-load the entire JSON value into memory as a JToken hierarchy.
The converter does not apply to lists whose items are also serialized as collections, e.g. List<string []>
The Boolean canWrite argument passed to the constructor controls whether to re-serialize single-element lists as JSON values or as JSON arrays.
The converter's ReadJson() uses the existingValue if pre-allocated so as to support populating of get-only list members.
Secondly, here is a version that works with other generic collections such as ObservableCollection<T>:
public class SingleOrArrayCollectionConverter<TCollection, TItem> : JsonConverter
where TCollection : ICollection<TItem>
{
// Adapted from this answer https://stackoverflow.com/a/18997172
// to https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n
// by Brian Rogers https://stackoverflow.com/users/10263/brian-rogers
readonly bool canWrite;
public SingleOrArrayCollectionConverter() : this(false) { }
public SingleOrArrayCollectionConverter(bool canWrite) { this.canWrite = canWrite; }
public override bool CanConvert(Type objectType)
{
return typeof(TCollection).IsAssignableFrom(objectType);
}
static void ValidateItemContract(IContractResolver resolver)
{
var itemContract = resolver.ResolveContract(typeof(TItem));
if (itemContract is JsonArrayContract)
throw new JsonSerializationException(string.Format("Item contract type {0} not supported.", itemContract));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
ValidateItemContract(serializer.ContractResolver);
if (reader.MoveToContent().TokenType == JsonToken.Null)
return null;
var list = (ICollection<TItem>)(existingValue ?? serializer.ContractResolver.ResolveContract(objectType).DefaultCreator());
if (reader.TokenType == JsonToken.StartArray)
serializer.Populate(reader, list);
else
list.Add(serializer.Deserialize<TItem>(reader));
return list;
}
public override bool CanWrite { get { return canWrite; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
ValidateItemContract(serializer.ContractResolver);
var list = value as ICollection<TItem>;
if (list == null)
throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), value.GetType()));
if (list.Count == 1)
{
foreach (var item in list)
{
serializer.Serialize(writer, item);
break;
}
}
else
{
writer.WriteStartArray();
foreach (var item in list)
serializer.Serialize(writer, item);
writer.WriteEndArray();
}
}
}
Then, if your model is using, say, an ObservableCollection<T> for some T, you could apply it as follows:
class Item
{
public string Email { get; set; }
public int Timestamp { get; set; }
public string Event { get; set; }
[JsonConverter(typeof(SingleOrArrayCollectionConverter<ObservableCollection<string>, string>))]
public ObservableCollection<string> Category { get; set; }
}
Notes:
In addition to the notes and restrictions for SingleOrArrayListConverter, the TCollection type must be read/write and have a parameterless constructor.
Demo fiddle with basic unit tests here.
To handle this you have to use a custom JsonConverter. But you probably already had that in mind.
You are just looking for a converter that you can use immediately. And this offers more than just a solution for the situation described.
I give an example with the question asked.
How to use my converter:
Place a JsonConverter Attribute above the property. JsonConverter(typeof(SafeCollectionConverter))
public class SendGridEvent
{
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("timestamp")]
public long Timestamp { get; set; }
[JsonProperty("category"), JsonConverter(typeof(SafeCollectionConverter))]
public string[] Category { get; set; }
[JsonProperty("event")]
public string Event { get; set; }
}
And this is my converter:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
namespace stackoverflow.question18994685
{
public class SafeCollectionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
//This not works for Populate (on existingValue)
return serializer.Deserialize<JToken>(reader).ToObjectCollectionSafe(objectType, serializer);
}
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
}
And this converter uses the following class:
using System;
namespace Newtonsoft.Json.Linq
{
public static class SafeJsonConvertExtensions
{
public static object ToObjectCollectionSafe(this JToken jToken, Type objectType)
{
return ToObjectCollectionSafe(jToken, objectType, JsonSerializer.CreateDefault());
}
public static object ToObjectCollectionSafe(this JToken jToken, Type objectType, JsonSerializer jsonSerializer)
{
var expectArray = typeof(System.Collections.IEnumerable).IsAssignableFrom(objectType);
if (jToken is JArray jArray)
{
if (!expectArray)
{
//to object via singel
if (jArray.Count == 0)
return JValue.CreateNull().ToObject(objectType, jsonSerializer);
if (jArray.Count == 1)
return jArray.First.ToObject(objectType, jsonSerializer);
}
}
else if (expectArray)
{
//to object via JArray
return new JArray(jToken).ToObject(objectType, jsonSerializer);
}
return jToken.ToObject(objectType, jsonSerializer);
}
public static T ToObjectCollectionSafe<T>(this JToken jToken)
{
return (T)ToObjectCollectionSafe(jToken, typeof(T));
}
public static T ToObjectCollectionSafe<T>(this JToken jToken, JsonSerializer jsonSerializer)
{
return (T)ToObjectCollectionSafe(jToken, typeof(T), jsonSerializer);
}
}
}
What does it do exactly?
If you place the converter attribute the converter will be used for this property. You can use it on a normal object if you expect a json array with 1 or no result. Or you use it on an IEnumerable where you expect a json object or json array. (Know that an array -object[]- is an IEnumerable)
A disadvantage is that this converter can only be placed above a property because he thinks he can convert everything. And be warned. A string is also an IEnumerable.
And it offers more than an answer to the question:
If you search for something by id you know that you will get an array back with one or no result.
The ToObjectCollectionSafe<TResult>() method can handle that for you.
This is usable for Single Result vs Array using JSON.net
and handle both a single item and an array for the same property
and can convert an array to a single object.
I made this for REST requests on a server with a filter that returned one result in an array but wanted to get the result back as a single object in my code. And also for a OData result response with expanded result with one item in an array.
Have fun with it.
Just wanted to add to #dbc excellent response above on the SingleOrArrayCollectionConverter. I was able to modify it to use with a stream from an HTTP client. Here is a snippet (you will have to set up the requestUrl (string) and the httpClient (using System.Net.Http;).
public async Task<IList<T>> HttpRequest<T>(HttpClient httpClient, string requestedUrl, CancellationToken cancellationToken)
{
using (var request = new HttpRequestMessage(HttpMethod.Get, requestedUrl))
using (var httpResponseMessage = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
{
if (httpResponseMessage.IsSuccessStatusCode)
{
using var stream = await httpResponseMessage.Content.ReadAsStreamAsync();
using var streamReader = new StreamReader(stream);
using var jsonTextReader = new JsonTextReader(streamReader );
var settings = new JsonSerializerSettings
{
// Pass true if you want single-item lists to be reserialized as single items
Converters = { new SingleOrArrayCollectionConverter(true) },
};
var jsonSerializer = JsonSerializer.Create(settings);
return jsonSerializer.Deserialize<List<T>>(jsonTextReader);
}
I apologize if there are missing brackets or misspellings, it was not easy to paste code in here.
I had a very similar Problem.
My Json Request was completly unknown for me.
I only knew.
There will be an objectId in it and some anonym key value pairs AND arrays.
I used it for an EAV Model i did:
My JSON Request:
{objectId": 2,
"firstName": "Hans",
"email" :[ "a#b.de","a#c.de"],
"name": "Andre",
"something" :["232","123"]
}
My Class i defined:
[JsonConverter(typeof(AnonyObjectConverter))]
public class AnonymObject
{
public AnonymObject()
{
fields = new Dictionary<string, string>();
list = new List<string>();
}
public string objectid { get; set; }
public Dictionary<string, string> fields { get; set; }
public List<string> list { get; set; }
}
and now that i want to deserialize unknown attributes with its value and arrays in it my Converter looks like that:
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
AnonymObject anonym = existingValue as AnonymObject ?? new AnonymObject();
bool isList = false;
StringBuilder listValues = new StringBuilder();
while (reader.Read())
{
if (reader.TokenType == JsonToken.EndObject) continue;
if (isList)
{
while (reader.TokenType != JsonToken.EndArray)
{
listValues.Append(reader.Value.ToString() + ", ");
reader.Read();
}
anonym.list.Add(listValues.ToString());
isList = false;
continue;
}
var value = reader.Value.ToString();
switch (value.ToLower())
{
case "objectid":
anonym.objectid = reader.ReadAsString();
break;
default:
string val;
reader.Read();
if(reader.TokenType == JsonToken.StartArray)
{
isList = true;
val = "ValueDummyForEAV";
}
else
{
val = reader.Value.ToString();
}
try
{
anonym.fields.Add(value, val);
}
catch(ArgumentException e)
{
throw new ArgumentException("Multiple Attribute found");
}
break;
}
}
return anonym;
}
So now everytime i get an AnonymObject i can iterate through the Dictionary and everytime there is my Flag "ValueDummyForEAV" i switch to the list, read the first line and split the values. After that i delete the first entry from the list and go on with iteration from the Dictionary.
Maybe someone has the same problem and can use this :)
Regards
Andre
You can use a JSONConverterAttribute as found here: http://james.newtonking.com/projects/json/help/
Presuming you have a class that looks like
public class RootObject
{
public string email { get; set; }
public int timestamp { get; set; }
public string smtpid { get; set; }
public string #event { get; set; }
public string category[] { get; set; }
}
You'd decorate the category property as seen here:
[JsonConverter(typeof(SendGridCategoryConverter))]
public string category { get; set; }
public class SendGridCategoryConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true; // add your own logic
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// do work here to handle returning the array regardless of the number of objects in
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// Left as an exercise to the reader :)
throw new NotImplementedException();
}
}
You don't need any custom converters, in this case I am usually creating a very simple JsonConstructor
public partial class Item
{
// ... all class properties
[JsonConstructor]
public Item(JToken category)
{
if (category.GetType().Name == "JArray")
Category = category.ToObject<List<string>>();
else
Category = new List<string> { category.ToString() };
}
public Item() { }
}
after this you can deserialize your json using common code
List<Item> items = JsonConvert.DeserializeObject<List<Item>>(json);
I found another solution that can handle the category as string or array by using object. This way I don´t need to mess up with the json serializer.
Please give it a look if you have the time and tell me what you think. https://github.com/MarcelloCarreira/sendgrid-csharp-eventwebhook
It´s based on the solution at https://sendgrid.com/blog/tracking-email-using-azure-sendgrid-event-webhook-part-1/ but I also added date conversion from timestamp, upgraded the variables to reflect current SendGrid model (and made categories work).
I also created a handler with basic auth as option. See the ashx files and the examples.
Thank you!
I'm looking for a way to transform this c# object:
class BaseClass
{
public string Value1 {get; set;}
public NestedObject nestedObject {get;set;}
}
class NestedObject
{
public string NestedValue1 {get; set;}
}
Into this json:
{
"Value1": "value1",
"NestedObject_NestedValue1": "nestedValue1"
}
By concatening the names of the nested parameters to their parent's name
Using normal serialization, this code: var json= JsonConvert.SerializeObject(baseClass);
Would instead return a json like this one:
{
"Value1": "value1",
"NestedObject": {
"NestedValue1": "nestedValue1"
}
}
I am sceptical about there being a way to deserialize a json like that back to an object tho.
Update:
As some asked what is the reason I'm trying to accomplish this:
The reason I asked this question is because I serialize this object to send as json metadata to a service that only allows referencing top level propreties in a way similar to this:
[Metadata_Value1] would return "value1"
However [Metadata_NestedObject_NestedValue1] doesn't work and there isn't any indication to there being a way to reference nested properties.
Taking this in consideration I hoped there would be some solution that would allow keeping the nested objects in my program but transforming them all to top properties when sending them to this service.
In the service I would then be able to do: [NestedObject_NestedValue1] and get the value "nestedValue1"
You can utilize a custom converter that looks like below :
public class NestedJsonConverter : JsonConverter
{
private readonly Type[] _types;
public NestedJsonConverter(params Type[] types)
{
_types = types;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JToken t = JToken.FromObject(value);
if (t.Type != JTokenType.Object)
{
t.WriteTo(writer);
}
else
{
JObject o = (JObject)t;
writer.WriteStartObject();
void writeNested(JObject target, object source, string prefix)
{
target.Properties().ToList().ForEach(p =>
{
var prop = source.GetType().GetProperty(p.Name);
var value = prop.GetValue(source);
var prefixed = string.IsNullOrEmpty(prefix) ? p.Name : $"{prefix}_{p.Name}";
if (p.Value.Type == JTokenType.Object)
{
writeNested((JObject)p.Value, value, prefixed);
}
else if (p.Value.Type == JTokenType.Array)
{
// you may need a more advanced handling in array scenarios
var arr = (JArray)p.Value;
writer.WritePropertyName(prefixed);
arr.WriteTo(writer);
}
else
{
writer.WritePropertyName(prefixed);
writer.WriteValue(value);
}
}
);
}
writeNested(o, value, "");
writer.WriteEndObject();
}
}
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 bool CanRead
{
get
{
return false;
}
}
public override bool CanConvert(Type objectType)
{
return _types.Any(t => t == objectType);
}
}
Dotnetfiddle
My application receives a JSON object as:
{
"names": [
"name_1",
"name_2"
]
}
I want to deserialize it to a list of Person object, defined as:
class Person
{
public string Name { set; get; }
}
Currently, I am deserializing the JSON object to a list of strings then create a list of Person manually like the following:
var names = JsonConvert.DeserializeObject<List<string>>(json);
var people = new List<Person>();
foreach(var name in names)
people.Add(new Person(){ Name = name });
Instead, I am interested in something like the following:
var people = JsonConvert.DeserializeObject<List<Person>>(json);
I am implementing the Person deserializer as:
public class PersonJsonConverter : JsonConverter
{
private readonly Dictionary<string, string> _propertyMappings;
public PersonJsonConverter()
{
_propertyMappings = new Dictionary<string, string>
{
{"name", nameof(Person.Name)}
};
}
public override bool CanConvert(Type objectType)
{
return objectType.GetTypeInfo().IsClass;
}
public override object ReadJson(
JsonReader reader,
Type objectType,
object existingValue,
JsonSerializer serializer)
{
object instance = Activator.CreateInstance(objectType);
var props = objectType.GetTypeInfo().DeclaredProperties.ToList();
JObject obj = JObject.Load(reader);
foreach (JProperty jsonProperty in obj.Properties())
{
if (!_propertyMappings.TryGetValue(jsonProperty.Name, out var name))
name = jsonProperty.Name;
PropertyInfo prop = props.FirstOrDefault(
pi => pi.CanWrite && pi.Name == name);
prop?.SetValue(
instance,
jsonProperty.Value.ToObject(prop.PropertyType, serializer));
}
return instance;
}
}
This deserializer can deserilize object such as:
{
"names": [
"name": "name_1",
"name": "name_2"
]
}
but not
{
"names": [
"name_1",
"name_2"
]
}
Note that my application can receive both types of JSON objects, so better to have a common deserializer for both types.
Please try below:
class Person
{
public string Name { set; get; }
}
public class PersonJsonConverter : JsonConverter
{
// private readonly Dictionary<string, string> _propertyMappings;
public PersonJsonConverter()
{
/*_propertyMappings = new Dictionary<string, string>
{
{"name", nameof(Person.Name)}
};*/
}
public override bool CanConvert(System.Type objectType)
{
return objectType.GetTypeInfo().IsClass;
}
public override object ReadJson(JsonReader reader, System.Type objectType, object existingValue, JsonSerializer serializer)
{
object instance = Activator.CreateInstance(objectType);
// List<T> implements the non-generic IList interface
IList list = (IList)instance;
var typeInfo = objectType.GetTypeInfo();
var props = typeInfo.DeclaredProperties.ToList();
PropertyInfo prop = props.FirstOrDefault(pi => pi.PropertyType == typeof(Person));
JObject obj = JObject.Load(reader);
var namesArray = obj["names"]; // you can use this instead of for loop on obj.Properties.
/*foreach (JProperty jsonProperty in obj.Properties())
{
if (jsonProperty.Name == "names")
{
var namesArray = JArray.Parse(jsonProperty.Value.ToString());
*/
if (namesArray.Type == JTokenType.Array && prop != null)
{
foreach (var ja in namesArray)
{
object personInstance = Activator.CreateInstance(prop.PropertyType);
PropertyInfo personNamePropInfo = prop.PropertyType.GetProperty(nameof(Person.Name));
personNamePropInfo.SetValue(personInstance,
Convert.ChangeType(ja, personNamePropInfo.PropertyType), null);
list.Add(personInstance); // Whatever you need to add
}
}
/* break;
}
}*/
return instance;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
and use it like below:
var samplejson = #"{
""names"": [
""name_1"",
""name_2""
]
}";
var obj = JsonConvert.DeserializeObject<List<Person>>(samplejson, new JsonConverter[] { new PersonJsonConverter() });
The other json does not seem to be valid json.
You have to deserialize names to something like a JArray, then foreach each entry with a new Person('name'). The JSON as you received it just doesn't match your desired schema. You have to manually transform it.
I'm trying to fix my SendGridPlus library to deal with SendGrid events, but I'm having some trouble with the inconsistent treatment of categories in the API.
In the following example payload taken from the SendGrid API reference, you'll notice that the category property for each item can either be a single string or an array of strings.
[
{
"email": "john.doe#sendgrid.com",
"timestamp": 1337966815,
"category": [
"newuser",
"transactional"
],
"event": "open"
},
{
"email": "jane.doe#sendgrid.com",
"timestamp": 1337966815,
"category": "olduser",
"event": "open"
}
]
It seems my options to make JSON.NET like this are fixing the string before it comes in, or configuring JSON.NET to accept the incorrect data. I'd rather not do any string parsing if I can get away with it.
Is there any other way I can handle this using Json.Net?
The best way to handle this situation is to use a custom JsonConverter.
Before we get to the converter, we'll need to define a class to deserialize the data into. For the Categories property that can vary between a single item and an array, define it as a List<string> and mark it with a [JsonConverter] attribute so that JSON.Net will know to use the custom converter for that property. I would also recommend using [JsonProperty] attributes so that the member properties can be given meaningful names independent of what is defined in the JSON.
class Item
{
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("timestamp")]
public int Timestamp { get; set; }
[JsonProperty("event")]
public string Event { get; set; }
[JsonProperty("category")]
[JsonConverter(typeof(SingleOrArrayConverter<string>))]
public List<string> Categories { get; set; }
}
Here is how I would implement the converter. Notice I've made the converter generic so that it can be used with strings or other types of objects as needed.
class SingleOrArrayConverter<T> : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(List<T>));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
if (token.Type == JTokenType.Array)
{
return token.ToObject<List<T>>();
}
return new List<T> { token.ToObject<T>() };
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Here is an short program demonstrating the converter in action with your sample data:
class Program
{
static void Main(string[] args)
{
string json = #"
[
{
""email"": ""john.doe#sendgrid.com"",
""timestamp"": 1337966815,
""category"": [
""newuser"",
""transactional""
],
""event"": ""open""
},
{
""email"": ""jane.doe#sendgrid.com"",
""timestamp"": 1337966815,
""category"": ""olduser"",
""event"": ""open""
}
]";
List<Item> list = JsonConvert.DeserializeObject<List<Item>>(json);
foreach (Item obj in list)
{
Console.WriteLine("email: " + obj.Email);
Console.WriteLine("timestamp: " + obj.Timestamp);
Console.WriteLine("event: " + obj.Event);
Console.WriteLine("categories: " + string.Join(", ", obj.Categories));
Console.WriteLine();
}
}
}
And finally, here is the output of the above:
email: john.doe#sendgrid.com
timestamp: 1337966815
event: open
categories: newuser, transactional
email: jane.doe#sendgrid.com
timestamp: 1337966815
event: open
categories: olduser
Fiddle: https://dotnetfiddle.net/lERrmu
EDIT
If you need to go the other way, i.e. serialize, while keeping the same format, you can implement the WriteJson() method of the converter as shown below. (Be sure to remove the CanWrite override or change it to return true, or else WriteJson() will never be called.)
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
List<T> list = (List<T>)value;
if (list.Count == 1)
{
value = list[0];
}
serializer.Serialize(writer, value);
}
Fiddle: https://dotnetfiddle.net/XG3eRy
I was working on this for ages, and thanks to Brian for his answer.
All I am adding is the vb.net answer!:
Public Class SingleValueArrayConverter(Of T)
sometimes-array-and-sometimes-object
Inherits JsonConverter
Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
Throw New NotImplementedException()
End Sub
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim retVal As Object = New [Object]()
If reader.TokenType = JsonToken.StartObject Then
Dim instance As T = DirectCast(serializer.Deserialize(reader, GetType(T)), T)
retVal = New List(Of T)() From { _
instance _
}
ElseIf reader.TokenType = JsonToken.StartArray Then
retVal = serializer.Deserialize(reader, objectType)
End If
Return retVal
End Function
Public Overrides Function CanConvert(objectType As Type) As Boolean
Return False
End Function
End Class
then in your class:
<JsonProperty(PropertyName:="JsonName)> _
<JsonConverter(GetType(SingleValueArrayConverter(Of YourObject)))> _
Public Property YourLocalName As List(Of YourObject)
Hope this saves you some time
As a minor variation to the great answer by Brian Rogers, here are two tweaked versions of SingleOrArrayConverter<T>.
Firstly, here is a version that works for all List<T> for every type T that is not itself a collection:
public class SingleOrArrayListConverter : JsonConverter
{
// Adapted from this answer https://stackoverflow.com/a/18997172
// to https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n
// by Brian Rogers https://stackoverflow.com/users/10263/brian-rogers
readonly bool canWrite;
readonly IContractResolver resolver;
public SingleOrArrayListConverter() : this(false) { }
public SingleOrArrayListConverter(bool canWrite) : this(canWrite, null) { }
public SingleOrArrayListConverter(bool canWrite, IContractResolver resolver)
{
this.canWrite = canWrite;
// Use the global default resolver if none is passed in.
this.resolver = resolver ?? new JsonSerializer().ContractResolver;
}
static bool CanConvert(Type objectType, IContractResolver resolver)
{
Type itemType;
JsonArrayContract contract;
return CanConvert(objectType, resolver, out itemType, out contract);
}
static bool CanConvert(Type objectType, IContractResolver resolver, out Type itemType, out JsonArrayContract contract)
{
if ((itemType = objectType.GetListItemType()) == null)
{
itemType = null;
contract = null;
return false;
}
// Ensure that [JsonObject] is not applied to the type.
if ((contract = resolver.ResolveContract(objectType) as JsonArrayContract) == null)
return false;
var itemContract = resolver.ResolveContract(itemType);
// Not implemented for jagged arrays.
if (itemContract is JsonArrayContract)
return false;
return true;
}
public override bool CanConvert(Type objectType) { return CanConvert(objectType, resolver); }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
Type itemType;
JsonArrayContract contract;
if (!CanConvert(objectType, serializer.ContractResolver, out itemType, out contract))
throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), objectType));
if (reader.MoveToContent().TokenType == JsonToken.Null)
return null;
var list = (IList)(existingValue ?? contract.DefaultCreator());
if (reader.TokenType == JsonToken.StartArray)
serializer.Populate(reader, list);
else
// Here we take advantage of the fact that List<T> implements IList to avoid having to use reflection to call the generic Add<T> method.
list.Add(serializer.Deserialize(reader, itemType));
return list;
}
public override bool CanWrite { get { return canWrite; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var list = value as ICollection;
if (list == null)
throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), value.GetType()));
// Here we take advantage of the fact that List<T> implements IList to avoid having to use reflection to call the generic Count method.
if (list.Count == 1)
{
foreach (var item in list)
{
serializer.Serialize(writer, item);
break;
}
}
else
{
writer.WriteStartArray();
foreach (var item in list)
serializer.Serialize(writer, item);
writer.WriteEndArray();
}
}
}
public static partial class JsonExtensions
{
public static JsonReader MoveToContent(this JsonReader reader)
{
while ((reader.TokenType == JsonToken.Comment || reader.TokenType == JsonToken.None) && reader.Read())
;
return reader;
}
internal static Type GetListItemType(this Type type)
{
// Quick reject for performance
if (type.IsPrimitive || type.IsArray || type == typeof(string))
return null;
while (type != null)
{
if (type.IsGenericType)
{
var genType = type.GetGenericTypeDefinition();
if (genType == typeof(List<>))
return type.GetGenericArguments()[0];
}
type = type.BaseType;
}
return null;
}
}
It can be used as follows:
var settings = new JsonSerializerSettings
{
// Pass true if you want single-item lists to be reserialized as single items
Converters = { new SingleOrArrayListConverter(true) },
};
var list = JsonConvert.DeserializeObject<List<Item>>(json, settings);
Notes:
The converter avoids the need to pre-load the entire JSON value into memory as a JToken hierarchy.
The converter does not apply to lists whose items are also serialized as collections, e.g. List<string []>
The Boolean canWrite argument passed to the constructor controls whether to re-serialize single-element lists as JSON values or as JSON arrays.
The converter's ReadJson() uses the existingValue if pre-allocated so as to support populating of get-only list members.
Secondly, here is a version that works with other generic collections such as ObservableCollection<T>:
public class SingleOrArrayCollectionConverter<TCollection, TItem> : JsonConverter
where TCollection : ICollection<TItem>
{
// Adapted from this answer https://stackoverflow.com/a/18997172
// to https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n
// by Brian Rogers https://stackoverflow.com/users/10263/brian-rogers
readonly bool canWrite;
public SingleOrArrayCollectionConverter() : this(false) { }
public SingleOrArrayCollectionConverter(bool canWrite) { this.canWrite = canWrite; }
public override bool CanConvert(Type objectType)
{
return typeof(TCollection).IsAssignableFrom(objectType);
}
static void ValidateItemContract(IContractResolver resolver)
{
var itemContract = resolver.ResolveContract(typeof(TItem));
if (itemContract is JsonArrayContract)
throw new JsonSerializationException(string.Format("Item contract type {0} not supported.", itemContract));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
ValidateItemContract(serializer.ContractResolver);
if (reader.MoveToContent().TokenType == JsonToken.Null)
return null;
var list = (ICollection<TItem>)(existingValue ?? serializer.ContractResolver.ResolveContract(objectType).DefaultCreator());
if (reader.TokenType == JsonToken.StartArray)
serializer.Populate(reader, list);
else
list.Add(serializer.Deserialize<TItem>(reader));
return list;
}
public override bool CanWrite { get { return canWrite; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
ValidateItemContract(serializer.ContractResolver);
var list = value as ICollection<TItem>;
if (list == null)
throw new JsonSerializationException(string.Format("Invalid type for {0}: {1}", GetType(), value.GetType()));
if (list.Count == 1)
{
foreach (var item in list)
{
serializer.Serialize(writer, item);
break;
}
}
else
{
writer.WriteStartArray();
foreach (var item in list)
serializer.Serialize(writer, item);
writer.WriteEndArray();
}
}
}
Then, if your model is using, say, an ObservableCollection<T> for some T, you could apply it as follows:
class Item
{
public string Email { get; set; }
public int Timestamp { get; set; }
public string Event { get; set; }
[JsonConverter(typeof(SingleOrArrayCollectionConverter<ObservableCollection<string>, string>))]
public ObservableCollection<string> Category { get; set; }
}
Notes:
In addition to the notes and restrictions for SingleOrArrayListConverter, the TCollection type must be read/write and have a parameterless constructor.
Demo fiddle with basic unit tests here.
To handle this you have to use a custom JsonConverter. But you probably already had that in mind.
You are just looking for a converter that you can use immediately. And this offers more than just a solution for the situation described.
I give an example with the question asked.
How to use my converter:
Place a JsonConverter Attribute above the property. JsonConverter(typeof(SafeCollectionConverter))
public class SendGridEvent
{
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("timestamp")]
public long Timestamp { get; set; }
[JsonProperty("category"), JsonConverter(typeof(SafeCollectionConverter))]
public string[] Category { get; set; }
[JsonProperty("event")]
public string Event { get; set; }
}
And this is my converter:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
namespace stackoverflow.question18994685
{
public class SafeCollectionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
//This not works for Populate (on existingValue)
return serializer.Deserialize<JToken>(reader).ToObjectCollectionSafe(objectType, serializer);
}
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
}
And this converter uses the following class:
using System;
namespace Newtonsoft.Json.Linq
{
public static class SafeJsonConvertExtensions
{
public static object ToObjectCollectionSafe(this JToken jToken, Type objectType)
{
return ToObjectCollectionSafe(jToken, objectType, JsonSerializer.CreateDefault());
}
public static object ToObjectCollectionSafe(this JToken jToken, Type objectType, JsonSerializer jsonSerializer)
{
var expectArray = typeof(System.Collections.IEnumerable).IsAssignableFrom(objectType);
if (jToken is JArray jArray)
{
if (!expectArray)
{
//to object via singel
if (jArray.Count == 0)
return JValue.CreateNull().ToObject(objectType, jsonSerializer);
if (jArray.Count == 1)
return jArray.First.ToObject(objectType, jsonSerializer);
}
}
else if (expectArray)
{
//to object via JArray
return new JArray(jToken).ToObject(objectType, jsonSerializer);
}
return jToken.ToObject(objectType, jsonSerializer);
}
public static T ToObjectCollectionSafe<T>(this JToken jToken)
{
return (T)ToObjectCollectionSafe(jToken, typeof(T));
}
public static T ToObjectCollectionSafe<T>(this JToken jToken, JsonSerializer jsonSerializer)
{
return (T)ToObjectCollectionSafe(jToken, typeof(T), jsonSerializer);
}
}
}
What does it do exactly?
If you place the converter attribute the converter will be used for this property. You can use it on a normal object if you expect a json array with 1 or no result. Or you use it on an IEnumerable where you expect a json object or json array. (Know that an array -object[]- is an IEnumerable)
A disadvantage is that this converter can only be placed above a property because he thinks he can convert everything. And be warned. A string is also an IEnumerable.
And it offers more than an answer to the question:
If you search for something by id you know that you will get an array back with one or no result.
The ToObjectCollectionSafe<TResult>() method can handle that for you.
This is usable for Single Result vs Array using JSON.net
and handle both a single item and an array for the same property
and can convert an array to a single object.
I made this for REST requests on a server with a filter that returned one result in an array but wanted to get the result back as a single object in my code. And also for a OData result response with expanded result with one item in an array.
Have fun with it.
Just wanted to add to #dbc excellent response above on the SingleOrArrayCollectionConverter. I was able to modify it to use with a stream from an HTTP client. Here is a snippet (you will have to set up the requestUrl (string) and the httpClient (using System.Net.Http;).
public async Task<IList<T>> HttpRequest<T>(HttpClient httpClient, string requestedUrl, CancellationToken cancellationToken)
{
using (var request = new HttpRequestMessage(HttpMethod.Get, requestedUrl))
using (var httpResponseMessage = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
{
if (httpResponseMessage.IsSuccessStatusCode)
{
using var stream = await httpResponseMessage.Content.ReadAsStreamAsync();
using var streamReader = new StreamReader(stream);
using var jsonTextReader = new JsonTextReader(streamReader );
var settings = new JsonSerializerSettings
{
// Pass true if you want single-item lists to be reserialized as single items
Converters = { new SingleOrArrayCollectionConverter(true) },
};
var jsonSerializer = JsonSerializer.Create(settings);
return jsonSerializer.Deserialize<List<T>>(jsonTextReader);
}
I apologize if there are missing brackets or misspellings, it was not easy to paste code in here.
I had a very similar Problem.
My Json Request was completly unknown for me.
I only knew.
There will be an objectId in it and some anonym key value pairs AND arrays.
I used it for an EAV Model i did:
My JSON Request:
{objectId": 2,
"firstName": "Hans",
"email" :[ "a#b.de","a#c.de"],
"name": "Andre",
"something" :["232","123"]
}
My Class i defined:
[JsonConverter(typeof(AnonyObjectConverter))]
public class AnonymObject
{
public AnonymObject()
{
fields = new Dictionary<string, string>();
list = new List<string>();
}
public string objectid { get; set; }
public Dictionary<string, string> fields { get; set; }
public List<string> list { get; set; }
}
and now that i want to deserialize unknown attributes with its value and arrays in it my Converter looks like that:
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
AnonymObject anonym = existingValue as AnonymObject ?? new AnonymObject();
bool isList = false;
StringBuilder listValues = new StringBuilder();
while (reader.Read())
{
if (reader.TokenType == JsonToken.EndObject) continue;
if (isList)
{
while (reader.TokenType != JsonToken.EndArray)
{
listValues.Append(reader.Value.ToString() + ", ");
reader.Read();
}
anonym.list.Add(listValues.ToString());
isList = false;
continue;
}
var value = reader.Value.ToString();
switch (value.ToLower())
{
case "objectid":
anonym.objectid = reader.ReadAsString();
break;
default:
string val;
reader.Read();
if(reader.TokenType == JsonToken.StartArray)
{
isList = true;
val = "ValueDummyForEAV";
}
else
{
val = reader.Value.ToString();
}
try
{
anonym.fields.Add(value, val);
}
catch(ArgumentException e)
{
throw new ArgumentException("Multiple Attribute found");
}
break;
}
}
return anonym;
}
So now everytime i get an AnonymObject i can iterate through the Dictionary and everytime there is my Flag "ValueDummyForEAV" i switch to the list, read the first line and split the values. After that i delete the first entry from the list and go on with iteration from the Dictionary.
Maybe someone has the same problem and can use this :)
Regards
Andre
You can use a JSONConverterAttribute as found here: http://james.newtonking.com/projects/json/help/
Presuming you have a class that looks like
public class RootObject
{
public string email { get; set; }
public int timestamp { get; set; }
public string smtpid { get; set; }
public string #event { get; set; }
public string category[] { get; set; }
}
You'd decorate the category property as seen here:
[JsonConverter(typeof(SendGridCategoryConverter))]
public string category { get; set; }
public class SendGridCategoryConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true; // add your own logic
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// do work here to handle returning the array regardless of the number of objects in
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// Left as an exercise to the reader :)
throw new NotImplementedException();
}
}
You don't need any custom converters, in this case I am usually creating a very simple JsonConstructor
public partial class Item
{
// ... all class properties
[JsonConstructor]
public Item(JToken category)
{
if (category.GetType().Name == "JArray")
Category = category.ToObject<List<string>>();
else
Category = new List<string> { category.ToString() };
}
public Item() { }
}
after this you can deserialize your json using common code
List<Item> items = JsonConvert.DeserializeObject<List<Item>>(json);
I found another solution that can handle the category as string or array by using object. This way I don´t need to mess up with the json serializer.
Please give it a look if you have the time and tell me what you think. https://github.com/MarcelloCarreira/sendgrid-csharp-eventwebhook
It´s based on the solution at https://sendgrid.com/blog/tracking-email-using-azure-sendgrid-event-webhook-part-1/ but I also added date conversion from timestamp, upgraded the variables to reflect current SendGrid model (and made categories work).
I also created a handler with basic auth as option. See the ashx files and the examples.
Thank you!
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);