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 have this model that I want to serialize and deserialize using Json.Net:
public struct RangeOrValue
{
public int Value { get; }
public int Min { get; }
public int Max { get; }
public bool IsRange { get; }
public RangeOrValue(int min, int max)
{
Min = min;
Max = max;
IsRange = true;
Value = 0;
}
public RangeOrValue(int value)
{
Min = 0;
Max = 0;
Value = value;
IsRange = false;
}
}
I have a special requirement for serialization. If the first constructor is used, then the value should be serialized as { "Min": <min>, "Max": <max> }.
But if the second constructor is used, then value should be serialized as <value>.
For example new RangeOrValue(0, 10) needs to be serialized as { "Min": 0, "Max": 10 } and new RangeOrValue(10) needs to be serialized as 10.
I wrote this custom converter to do this task:
public class RangeOrValueConverter : JsonConverter<RangeOrValue>
{
public override void WriteJson(JsonWriter writer, RangeOrValue value, JsonSerializer serializer)
{
if (value.IsRange)
{
// Range values are stored as objects
writer.WriteStartObject();
writer.WritePropertyName("Min");
writer.WriteValue(value.Min);
writer.WritePropertyName("Max");
writer.WriteValue(value.Max);
writer.WriteEndObject();
}
else
{
writer.WriteValue(value.Value);
}
}
public override RangeOrValue ReadJson(JsonReader reader, Type objectType, RangeOrValue existingValue, bool hasExistingValue, JsonSerializer serializer)
{
reader.Read();
// If the type is range, then first token should be property name ("Min" property)
if (reader.TokenType == JsonToken.PropertyName)
{
// Read min value
int min = reader.ReadAsInt32() ?? 0;
// Read next property name
reader.Read();
// Read max value
int max = reader.ReadAsInt32() ?? 0;
// Read object end
reader.Read();
return new RangeOrValue(min, max);
}
// Read simple int
return new RangeOrValue(Convert.ToInt32(reader.Value));
}
}
To test the functionality, I wrote this simple test:
[TestFixture]
public class RangeOrValueConverterTest
{
public class Model
{
public string Property1 { get; set; }
public RangeOrValue Value { get; set; }
public string Property2 { get; set; }
public RangeOrValue[] Values { get; set; }
public string Property3 { get; set; }
}
[Test]
public void Serialization_Value()
{
var model = new Model
{
Value = new RangeOrValue(10),
Values = new[] {new RangeOrValue(30), new RangeOrValue(40), new RangeOrValue(50),},
Property1 = "P1",
Property2 = "P2",
Property3 = "P3"
};
string json = JsonConvert.SerializeObject(model, new RangeOrValueConverter());
var deserializedModel = JsonConvert.DeserializeObject<Model>(json, new RangeOrValueConverter());
Assert.AreEqual(model, deserializedModel);
}
}
When I run the test, Object serializes successfully. But when it tries to deserialize it back, I receive this error:
Newtonsoft.Json.JsonReaderException : Could not convert string to integer: P2. Path 'Property2', line 1, position 46.
The stack trace leads to line int min = reader.ReadAsInt32() ?? 0;.
I think I'm doing something wrong in converter that causes Json.Net to provide wrong values to the converter. But I can't quite figure it out. Any ideas?
Your basic problem is that, at the beginning of ReadJson(), you unconditionally call Read() to advance the reader past the current token:
public override RangeOrValue ReadJson(JsonReader reader, Type objectType, RangeOrValue existingValue, bool hasExistingValue, JsonSerializer serializer)
{
reader.Read();
However, if the current token is an integer corresponding to a RangeOrValue with a single value, then you have just skipped past that value, leaving the reader positioned on whatever comes next. Instead you need to process the current value when that value is of type JsonToken.Integer.
That being said, there are several other possible issues with your converter, mostly related to the fact that you assume that the incoming JSON is in a particular format, rather than validating that fact:
According to the JSON standard an object is an unordered set of name/value pairs but ReadJson() assumes a specific property order.
ReadJson() doesn't skip past or error on unknown properties.
ReadJson() doesn't error on truncated files.
ReadJson() doesn't error on unexpected token types (say, an array instead of an object or integer).
If the JSON file contains comments (which are not included in the JSON standard but are supported by Json.NET) then ReadJson() will not handle this.
The converter doesn't handle Nullable<RangeOrValue> members.
Note that if you inherit from JsonConverter<T>, then you will have to write separate converters for T and Nullable<T>. Thus, for structs, I think it's easier to inherit from the base class JsonConverter.
A JsonConverter that handles these issues would look something like the following:
public class RangeOrValueConverter : JsonConverter
{
const string MinName = "Min";
const string MaxName = "Max";
public override bool CanConvert(Type objectType)
{
return objectType == typeof(RangeOrValue) || Nullable.GetUnderlyingType(objectType) == typeof(RangeOrValue);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var range = (RangeOrValue)value;
if (range.IsRange)
{
// Range values are stored as objects
writer.WriteStartObject();
writer.WritePropertyName(MinName);
writer.WriteValue(range.Min);
writer.WritePropertyName(MaxName);
writer.WriteValue(range.Max);
writer.WriteEndObject();
}
else
{
writer.WriteValue(range.Value);
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
switch (reader.MoveToContent().TokenType)
{
case JsonToken.Null:
// nullable RangeOrValue; return null.
return null;
case JsonToken.Integer:
return new RangeOrValue(reader.ValueAsInt32());
case JsonToken.StartObject:
int? min = null;
int? max = null;
var done = false;
while (!done)
{
// Read the next token skipping comments if any
switch (reader.ReadToContentAndAssert().TokenType)
{
case JsonToken.PropertyName:
var name = (string)reader.Value;
if (name.Equals(MinName, StringComparison.OrdinalIgnoreCase))
// ReadAsInt32() reads the NEXT token as an Int32, thus advancing past the property name.
min = reader.ReadAsInt32();
else if (name.Equals(MaxName, StringComparison.OrdinalIgnoreCase))
max = reader.ReadAsInt32();
else
// Unknown property name. Skip past it and its value.
reader.ReadToContentAndAssert().Skip();
break;
case JsonToken.EndObject:
done = true;
break;
default:
throw new JsonSerializationException(string.Format("Invalid token type {0} at path {1}", reader.TokenType, reader.Path));
}
}
if (max != null && min != null)
return new RangeOrValue(min.Value, max.Value);
throw new JsonSerializationException(string.Format("Missing min or max at path {0}", reader.Path));
default:
throw new JsonSerializationException(string.Format("Invalid token type {0} at path {1}", reader.TokenType, reader.Path));
}
}
}
Using the extension methods:
public static partial class JsonExtensions
{
public static int ValueAsInt32(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType != JsonToken.Integer)
throw new JsonSerializationException("Value is not Int32");
try
{
return Convert.ToInt32(reader.Value, NumberFormatInfo.InvariantInfo);
}
catch (Exception ex)
{
// Wrap the system exception in a serialization exception.
throw new JsonSerializationException(string.Format("Invalid integer value {0}", reader.Value), ex);
}
}
public static JsonReader ReadToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
while (reader.Read())
{
if (reader.TokenType != JsonToken.Comment)
return reader;
}
throw new JsonReaderException(string.Format("Unexpected end at path {0}", reader.Path));
}
public static JsonReader MoveToContent(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None)
if (!reader.Read())
return reader;
while (reader.TokenType == JsonToken.Comment && reader.Read())
;
return reader;
}
}
However, if you're willing to pay a slight performance penalty, the converter can be simplified by serializing and deserializing a DTO as shown below, which uses the same extension method class:
public class RangeOrValueConverter : JsonConverter
{
class RangeDTO
{
public int Min, Max;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(RangeOrValue) || Nullable.GetUnderlyingType(objectType) == typeof(RangeOrValue);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var range = (RangeOrValue)value;
if (range.IsRange)
{
var dto = new RangeDTO { Min = range.Min, Max = range.Max };
serializer.Serialize(writer, dto);
}
else
{
writer.WriteValue(range.Value);
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
switch (reader.MoveToContent().TokenType)
{
case JsonToken.Null:
// nullable RangeOrValue; return null.
return null;
case JsonToken.Integer:
return new RangeOrValue(reader.ValueAsInt32());
default:
var dto = serializer.Deserialize<RangeDTO>(reader);
return new RangeOrValue(dto.Min, dto.Max);
}
}
}
Demo fiddle showing both converters here.
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 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>.
When serialise, I write ClassName to object into _CurrentClassName property. And when read json with JSON.Net library I need to change object to value from this property.
{
"Field1": 0,
"Field2": "34",
"_CurrentClassName": "MyCustomClass"
}
class CustomJsonConverter : JsonConverter
{
...
public override bool CanConvert(Type objectType)
{
return objectType.IsClass;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var value = existingValue;
if (reader.TokenType == JsonToken.Null)
{
return null;
}
else if (reader.TokenType == JsonToken.StartObject)
{
JObject jObject = JObject.Load(reader);
JToken jToken;
if (jObject.TryGetValue("_CurrentClassName", out jToken))
{
var t = jToken.Value<string>();
Type tt = Type.GetType(objectType.Namespace + "." + t);
value = Activator.CreateInstance(tt);
return value;
}
}
return serializer.Deserialize(reader);
}
...
}
Once the object type has been inferred and the object has been instantiated, you can use JsonSerializer.Populate(jObject.CreateReader()) to populate it.
For instance:
public abstract class CustomJsonConverterBase : JsonConverter
{
protected abstract Type InferType(JToken token, Type objectType, object existingValue);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var token = JToken.Load(reader);
var actualType = InferType(token, objectType, existingValue);
if (existingValue == null || existingValue.GetType() != actualType)
{
var contract = serializer.ContractResolver.ResolveContract(actualType);
existingValue = contract.DefaultCreator();
}
using (var subReader = token.CreateReader())
{
// Using "populate" avoids infinite recursion.
serializer.Populate(subReader, existingValue);
}
return existingValue;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public class CustomJsonConverter : CustomJsonConverterBase
{
public const string ClassPropertyName = #"_CurrentClassName";
protected override Type InferType(JToken token, Type objectType, object existingValue)
{
if (token is JObject)
{
var typeName = (string)token[ClassPropertyName];
if (typeName != null)
{
var actualType = Type.GetType(objectType.Namespace + "." + typeName);
if (actualType != null)
return actualType;
}
}
return objectType;
}
public override bool CanConvert(Type objectType)
{
throw new NotImplementedException();
}
}
CustomCreationConverter<T> also uses serializer.Populate() to fill in the just-allocated object so this is a standard way to solve this problem.
Note that you are partially replicating Json.NET's built-in TypeNameHandling functionality.
If you're not married to the _CurrentClassName property name or its value syntax you could use Json.Net's built in handling of types.
When serializing or deserializing you can pass in a JsonSerializerSettings object controlling the serialization or deserialization.
On this object you can set a TypeNameHandling property that controls how Json.Net serializes and deserializes the exact type being processed.
Here is a LINQPad example:
void Main()
{
var t = new Test { Key = 42, Value = "Meaning of life" };
var json = JsonConvert.SerializeObject(
t, Newtonsoft.Json.Formatting.Indented,
new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All });
Console.WriteLine(json);
var obj =JsonConvert.DeserializeObject(json,
new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All });
Console.WriteLine(obj.GetType().FullName);
}
public class Test
{
public int Key { get; set; }
public string Value { get; set; }
}
Output:
{
"$type": "UserQuery+Test, LINQPadQuery",
"Key": 42,
"Value": "Meaning of life"
}
UserQuery+Test
Here you can see that the type of the object being returned from deserialization is the Test class.