Deserialize LocalDate from version 1 to version 2 - json.net

Unfortunately we didn't add the NodaTime.Serialization configuration to the NEventStore wiring when we started this project.
That means we have JSON documents in NEventStore like this.
Note. Simplified view. Not actually an event representation.
dates.json
{
"$type": "NodatimeIssueTest.Product, NodatimeIssueTest",
"FirstDate": {
"$type": "NodaTime.LocalDate, NodaTime",
"ticks": 12304224000000000,
"calendar": "ISO"
},
"SecondDate": {
"$type": "System.Nullable`1[[NodaTime.LocalDate, NodaTime]], mscorlib",
"ticks": 12304224000000000,
"calendar": "ISO"
},
"OtherDates": [
{
"$type": "NodaTime.LocalDate, NodaTime",
"ticks": 12304224000000000,
"calendar": "ISO"
}
],
"FirstDateTime": {
"$type": "NodaTime.LocalDateTime, NodaTime",
"ticks": 12304734100000000,
"calendar": "ISO"
},
"FirstInstant": {
"$type": "NodaTime.Instant, NodaTime",
"ticks": 12304734100000000
}
}
Instead of this
{
"$type": "NodatimeIssueTest.Product, NodatimeIssueTest",
"FirstDate": "2008-12-28",
"SecondDate": "2008-12-28",
"OtherDates": [
"2008-12-28"
],
"FirstDateTime": "2008-12-28T14:10:10",
"FirstInstant": "2008-12-28T14:10:10Z"
}
Because of this we have hard time to upgrade to latest NodaTime package because we can't deserialize the json document anymore.
One solution would be to read all the NEventStore commits and serialize with correct NodaTime parsers. But if this could be avoided I would be happy.
Another option would be to do custom conversion and data binding.
But that requires low level Nodatime logic. Although we don't need to cover all calendars for example.

Solution with custom converters and binder for dates.json example in question
using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using NodaTime;
using NodaTime.Serialization.JsonNet;
using NUnit.Framework;
namespace NodatimeIssueTest
{
[TestFixture]
public class TestClass
{
[Test]
public void Deserialize()
{
var serializer = new JsonSerializer();
serializer.NullValueHandling = NullValueHandling.Ignore;
serializer.TypeNameHandling = TypeNameHandling.Objects;
//Custom converters and binder
serializer.Converters.Add(new NodaLocalDateConverter());
serializer.Converters.Add(new NodaLocalDateTimeConverter());
serializer.Converters.Add(new NodaInstantConverter());
serializer.SerializationBinder = new CustomBinder();
using (var sr = new StreamReader($#"{TestContext.CurrentContext.TestDirectory}\dates.json"))
using (var reader = new JsonTextReader(sr))
{
var product = serializer.Deserialize<Product>(reader);
Assert.AreEqual(new LocalDate(2008, 12, 28), product.FirstDate);
Assert.AreEqual(new LocalDate(2008, 12, 28), product.SecondDate);
Assert.AreEqual(new LocalDate(2008, 12, 28), product.OtherDates[0]);
Assert.AreEqual(new LocalDateTime(2008, 12, 28, 14, 10, 10), product.FirstDateTime);
Assert.AreEqual(Instant.FromUtc(2008, 12, 28, 14, 10, 10), product.FirstInstant);
}
}
}
public class Product
{
public LocalDate FirstDate { get; set; }
public LocalDate? SecondDate { get; set; }
public List<LocalDate> OtherDates { get; set; }
public LocalDateTime FirstDateTime { get; set; }
public Instant FirstInstant { get; set; }
}
public class NodaLocalDateConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanRead => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return null;
if (reader.TokenType == JsonToken.StartObject)
{
var custom = (CustomLocalDate) serializer.Deserialize(reader, typeof(CustomLocalDate));
var dateTime = new DateTime(custom.Ticks, DateTimeKind.Utc).AddTicks(new DateTime(1970, 1, 1).Ticks);
var local = LocalDate.FromDateTime(dateTime, CalendarSystem.Iso);
return local;
}
return null;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(LocalDate) || objectType == typeof(LocalDate?);
}
}
public class NodaLocalDateTimeConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanRead => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return null;
if (reader.TokenType == JsonToken.StartObject)
{
var custom = (CustomLocalDateTime) serializer.Deserialize(reader, typeof(CustomLocalDateTime));
var dateTime = new DateTime(custom.Ticks, DateTimeKind.Utc).AddTicks(new DateTime(1970, 1, 1).Ticks);
var local = LocalDateTime.FromDateTime(dateTime, CalendarSystem.Iso);
return local;
}
return null;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(LocalDateTime) || objectType == typeof(LocalDateTime?);
}
}
public class NodaInstantConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanRead => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return null;
if (reader.TokenType == JsonToken.StartObject)
{
var custom = (CustomInstant) serializer.Deserialize(reader, typeof(CustomInstant));
var dateTime = new DateTime(custom.Ticks, DateTimeKind.Utc).AddTicks(new DateTime(1970, 1, 1).Ticks);
var local = Instant.FromDateTimeUtc(dateTime);
return local;
}
return null;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Instant) || objectType == typeof(Instant?);
}
}
public class CustomBinder : DefaultSerializationBinder
{
public override Type BindToType(string assemblyName, string typeName)
{
switch (typeName)
{
case "NodaTime.LocalDate": return typeof(CustomLocalDate);
case "System.Nullable`1[[NodaTime.LocalDate, NodaTime]]": return typeof(CustomLocalDate);
case "NodaTime.LocalDateTime": return typeof(CustomLocalDateTime);
case "System.Nullable`1[[NodaTime.LocalDateTime, NodaTime]]": return typeof(CustomLocalDateTime);
case "NodaTime.Instant": return typeof(CustomInstant);
case "System.Nullable`1[[NodaTime.Instant, NodaTime]]": return typeof(CustomInstant);
default: return base.BindToType(assemblyName, typeName);
}
}
}
public class CustomLocalDate
{
[JsonProperty("ticks")] public long Ticks { get; set; }
[JsonProperty("calendar")] public string Calendar { get; set; }
}
public class CustomLocalDateTime
{
[JsonProperty("ticks")] public long Ticks { get; set; }
[JsonProperty("calendar")] public string Calendar { get; set; }
}
public class CustomInstant
{
[JsonProperty("ticks")] public long Ticks { get; set; }
}
}

Related

Deserializing JSON with Nullable DateTime property throws Newtonsoft.Json.JsonReaderException

I'm trying to deserialize following JSON string below
[
{
"gerceklesenTarih": 1487710800000,
"deletedDate": null
}
]
to Dto object below (I simplified the class for clarification).
public class Dto
{
public DateTime? gerceklesenTarih { get; set; }
public DateTime? deletedDate { get; set; }
}
with code :
if(File.Exists("aa - Copy.txt"))
{
var OdemePlaniStr = File.ReadAllText("aa - Copy.txt");
var settings = new JsonSerializerSettings
{
Converters = { new DateTimeConverter() },
};
var resultOdemePlani = JsonConvert.DeserializeObject<List<Dto>>(OdemePlaniStr, settings);
}
As the JSON object I consume sends datetime values as long, I have to use a Converter class below
public class DateTimeConverter : Newtonsoft.Json.JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
var t = (long)reader.Value;
//return DateTime.Parse(t.ToString());
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(t);
}
catch (Exception ex)
{
throw ex;
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(Convert.ToInt64(((DateTime)value - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds));
}
}
Deserializing non nullable DateTime fields have no problem, however when JSON object has a value in its nullable DateTime field, it throws :
Newtonsoft.Json.JsonReaderException: 'Unexpected character encountered while parsing value: 1. Path '[0].gerceklesenTarih', line 4, position 25.'
When I debug the code, it doesn't hit DateTimeConverter class even if it should. Additionally below notation works
"gerceklesenTarih": "/Date(1487710800000)/"
What could be the reason?
I figured it, forgot to add nullable type to CanConvert method of DateTimeConverter
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime);
}
It works as expected now

Creating a custom converter to write json as a Dictionary<string, IObject> from an object of String Key IObject Value

I am trying to create a custom JsonConverter to follow a third party API with a rather complicated object structure, and am a bit hamstrung on a few things. Note I am using .NET 4.7.2, not Core.
I have source objects that look like this, which I have created as objects rather than dictionaries in order to easily integrate them with FluentValidation. The objects will represent json objects in the foreign API. The "Value" is an interfaced item which is eventually converted to one of about 20 concrete types.
public class PDItem : IDictionaryObject<List<IItem>>
{
[JsonProperty]
public string Key { get; set; }
[JsonProperty]
public List<IItem> Value { get; set; }
}
The source json for such an object is a dictionary, whereby the Key is the property, and the value is one of the 20 object types, as so:
{
"checked": [
{
"elementType": "input",
"inputType": "checkbox",
"name": "another_checkbox",
"label": "Another checkbox",
"checked": true
}
]
}
In this case, the object in C# would look something like this:
PDItem item = new PDItem() {
Key = "checked",
Value = new List<IItem>() {
new ConcreteTypeA () {
elementType = "input",
inputType = "checkbox",
name = "another_checkbox",
label = "Another checkbox",
#checked = true
}
}
};
For reference, my "PDItem"s implement the following interfaces:
public interface IDictionaryObject<T>
{
string Key { get; set; }
T Value { get; set; }
}
[JsonConverter(typeof(IItemConverter))]
public interface IItem: IElement
{
}
[JsonConverter(typeof(ElementConverter))]
public interface IElement
{
string elementType { get; }
}
I was able to convert some concrete types (without having to do any tricky dictionary to object conversions), below is a working example of the ElementConverter attacked to my IElement interface (IItem uses the same pattern, and the same JsonCreationConverter class):
public class ElementConverter : JsonCreationConverter<IElement>
{
protected override IElement Create(Type objectType, JObject jObject)
{
//TODO: Add objects to ElementConverter as they come online.
switch (jObject["elementType"].Value<string>())
{
case ElementTypeDescriptions.FirstType:
return new FirstType();
case ElementTypeDescriptions.SecondType:
return new SecondType();
case ElementTypeDescriptions.ThirdType:
return new ThirdType();
case ElementTypeDescriptions.FourthType:
case ElementTypeDescriptions.FifthType:
default:
throw new NotImplementedException("This object type is not yet implemented.");
}
}
}
public abstract class JsonCreationConverter<T> : JsonConverter
{
protected abstract T Create(Type objectType, JObject jObject);
public override bool CanConvert(Type objectType)
{
return typeof(T) == objectType;
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
try
{
var jObject = JObject.Load(reader);
var target = Create(objectType, jObject);
serializer.Populate(jObject.CreateReader(), target);
return target;
}
catch (JsonReaderException)
{
return null;
}
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
What makes my scenario so tricky is not that I am converting objects to dictionaries and back, but that my object values are other concrete objects (interfaced). I want to write a custom JsonConverter to serialize and deserialize these objects, but have no idea how to read and write the json in the methods below, let alone if what I am attempting to do is even possible. Any assistance would be greatly appreciated!
public class PDItemConverter: JsonConverter<PDItem>
{
public override void WriteJson(JsonWriter writer, PDItem value, JsonSerializer serializer)
{
/// DO STUFF
}
public override PDItem ReadJson(JsonReader reader, Type objectType, PDItem existingValue,
bool hasExistingValue, JsonSerializer serializer)
{
/// DO STUFF
}
}
EDITED PER DBC's request:
Apologies for the complicated question DBC, and I greatly appreciate your time! Obviously, I'm a bit new to posting to stack overflow (long time lurker as they say).
Below is full working code that will run in .net fiddle (or in a console application if you simply add a namespace and json.net 12.x packages to a new .NET 4.7.2 console project). Second apologies that it is still a bit long and complicated, It is actually greatly simplified thanks to the omission of the Validation code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
public class PDItem : IDictionaryObject<List<IItem>>
{
[JsonProperty]
public string Key { get; set; }
[JsonProperty]
public List<IItem> Value { get; set; }
}
public interface IDictionaryObject<T>
{
string Key { get; set; }
T Value { get; set; }
}
[JsonConverter(typeof(IItemConverter))]
public interface IItem : IElement
{
string itemType { get; }
}
[JsonConverter(typeof(ElementConverter))]
public interface IElement
{
string elementType { get; }
}
public class ElementConverter : JsonCreationConverter<IElement>
{
protected override IElement Create(Type objectType, JObject jObject)
{
//TODO: Add objects to ElementConverter as they come online.
switch (jObject["elementType"].Value<string>())
{
case ElementTypeDescriptions.FirstType:
return new FirstType();
case ElementTypeDescriptions.SecondType:
return new SecondType();
//case ElementTypeDescriptions.ThirdType:
// return new ThirdType();
//case ElementTypeDescriptions.FourthType:
//case ElementTypeDescriptions.FifthType:
default:
throw new NotImplementedException("This object type is not yet implemented.");
}
}
}
public class IItemConverter : JsonCreationConverter<IItem>
{
protected override IItem Create(Type objectType, JObject jObject)
{
switch (jObject["itemType"].Value<string>())
{
case ItemTypeDescriptions.FirstItemType:
return new FirstItemType();
case ItemTypeDescriptions.SecondItemType:
return new SecondItemType();
default:
throw new NotImplementedException("This object type is not yet implemented.");
}
}
}
/// <summary>
/// Used constants rather than an enum to allow for use in switch statements. Provided by third party to us to identify their classes across the API.
/// </summary>
public class ElementTypeDescriptions
{
public const string FirstType = "firstTypeId";
public const string SecondType = "secondTypeId";
public const string ThirdType = "thirdTypeId";
public const string FourthType = "fourthTypeId";
public const string FifthType = "fifthTypeId";
}
/// <summary>
/// Used constants rather than an enum to allow for use in switch statements. Provided by third party to us to identify their classes across the API.
/// </summary>
public class ItemTypeDescriptions
{
public const string FirstItemType = "firstItemTypeId";
public const string SecondItemType = "secondItemTypeId";
}
/*** CONCRETE OBJECTS ***/
public class FirstType : IElement
{
public string elementType { get { return ElementTypeDescriptions.FirstType; } }
public string name { get; set; }
}
public class SecondType : IElement
{
public string elementType { get { return ElementTypeDescriptions.FirstType; } }
public string label { get; set; }
}
public class FirstItemType : IItem
{
public string elementType { get { return ElementTypeDescriptions.FourthType; } }
public string itemType { get { return ItemTypeDescriptions.FirstItemType; } }
public string reference { get; set; }
}
public class SecondItemType : IItem
{
public string elementType { get { return ElementTypeDescriptions.FourthType; } }
public string itemType { get { return ItemTypeDescriptions.FirstItemType; } }
public string database { get; set; }
}
/*** END CONCRETE OBJECTS ***/
public class PDItemConverter : JsonConverter<PDItem>
{
public override void WriteJson(JsonWriter writer, PDItem value, JsonSerializer serializer)
{
/// THIS CODE TO BE WRITTEN TO ANSWER THE QUESTION
/// DO STUFF
throw new NotImplementedException("THIS CODE TO BE WRITTEN TO ANSWER THE QUESTION");
}
public override PDItem ReadJson(JsonReader reader, Type objectType, PDItem existingValue,
bool hasExistingValue, JsonSerializer serializer)
{
/// THIS CODE TO BE WRITTEN TO ANSWER THE QUESTION
/// DO STUFF
throw new NotImplementedException("THIS CODE TO BE WRITTEN TO ANSWER THE QUESTION");
}
}
public abstract class JsonCreationConverter<T> : JsonConverter
{
protected abstract T Create(Type objectType, JObject jObject);
public override bool CanConvert(Type objectType)
{
return typeof(T) == objectType;
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
try
{
var jObject = JObject.Load(reader);
var target = Create(objectType, jObject);
serializer.Populate(jObject.CreateReader(), target);
return target;
}
catch (JsonReaderException)
{
return null;
}
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
class TestClass
{
public static void Test()
{
var json = GetJson();
var item = JsonConvert.DeserializeObject<PDItem>(json);
var json2 = JsonConvert.SerializeObject(item, Formatting.Indented);
Console.WriteLine(json2);
}
static string GetJson()
{
var json = #"{
""checked"": [
{
""elementType"": ""input"",
""inputType"": ""checkbox"",
""name"": ""another_checkbox"",
""label"": ""Another checkbox"",
""checked"": true
}
]
}
";
return json;
}
}
public class Program
{
public static void Main()
{
Console.WriteLine("Environment version: " + Environment.Version);
Console.WriteLine("Json.NET version: " + typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine();
try
{
TestClass.Test();
}
catch (Exception ex)
{
Console.WriteLine("Failed with unhandled exception: ");
Console.WriteLine(ex);
throw;
}
}
}
One of the easiest ways to write a JsonConverter is to map the object to be serialized to some DTO, then (de)serialize the DTO. Since your PDItem looks like a single dictionary key/value pair and is serialized like a dictionary, the easiest DTO to use would be an actual dictionary, namely Dictionary<string, List<IItem>>.
Thus your PDItemConverter can be written as follows:
public class PDItemConverter: JsonConverter<PDItem>
{
public override void WriteJson(JsonWriter writer, PDItem value, JsonSerializer serializer)
{
// Convert to a dictionary DTO and serialize
serializer.Serialize(writer, new Dictionary<string, List<IItem>> { { value.Key, value.Value } });
}
public override PDItem ReadJson(JsonReader reader, Type objectType, PDItem existingValue,
bool hasExistingValue, JsonSerializer serializer)
{
// Deserialize as a dictionary DTO and map to a PDItem
var dto = serializer.Deserialize<Dictionary<string, List<IItem>>>(reader);
if (dto == null)
return null;
if (dto.Count != 1)
throw new JsonSerializationException(string.Format("Incorrect number of dictionary keys: {0}", dto.Count));
var pair = dto.First();
existingValue = hasExistingValue ? existingValue : new PDItem();
existingValue.Key = pair.Key;
existingValue.Value = pair.Value;
return existingValue;
}
}
Since you are (de)serializing using the incoming serializer any converters associated to nested types such as IItem will get picked up and used automatically.
In addition, in JsonCreationConverter<T> you need to override CanWrite and return false. This causes the serializer to fall back to default serialization when writing JSON as explained in this answer to How to use default serialization in a custom JsonConverter. Also, I don't recommend catching and swallowing JsonReaderException. This exception is thrown when the JSON file itself is malformed, e.g. by being truncated. Ignoring this exception and continuing can occasionally force Newtonsoft to fall into an infinite loop. Instead, propagate the exception up to the application:
public abstract class JsonCreationConverter<T> : JsonConverter
{
// Override CanWrite and return false
public override bool CanWrite { get { return false; } }
protected abstract T Create(Type objectType, JObject jObject);
public override bool CanConvert(Type objectType)
{
return typeof(T) == objectType;
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
var jObject = JObject.Load(reader);
var target = Create(objectType, jObject);
serializer.Populate(jObject.CreateReader(), target);
return target;
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Demo fiddle here.

Disable DateParseHandling on document deserialization

We have documents on DocumentDB that store a date on ISO 8601 format. These dates are stored as strings:
{
"CreatedOn": "2016-04-15T14:54:40Z",
"Title": "Some title",
"id": "xxx-xxx-xxxx-xxx-xxx-xxx"
}
We are using the latest Azure DocumentDB SDK (1.7.0) on a WebAPI that uses ASP.NET Core.
The C# class that maps our documents have the "CreatedOn" property as a string.
public class Item
{
public string CreatedOn { get; set; }
public string id { get; set; }
public string Title { get; set; }
}
The problem is that when we read a document and the SDK deserializes it, it tries to convert it to a DateTime and then back to a string. Resulting in:
{
"CreatedOn": "15/04/2016 14:54:40",
"Title": "Some title",
"id": "xxx-xxx-xxxx-xxx-xxx-xxx"
}
What I need is to the SDK to leave the values untouched. I tried setting the default SerializerSettings to avoid the date parsing:
services.AddMvc().AddJsonOptions(opts =>
{
opts.SerializerSettings.DateParseHandling = Newtonsoft.Json.DateParseHandling.None;
});
But it didn't work.
I tried using a JsonConverter attribute but the problem is that on the ReadJson override method, the reader already parsed the string value to a DateTime.
class StringJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override bool CanRead
{
get { return true; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
reader.Value <-- already a parsed DateTime
}
}
Any ideas on how to overcome this auto parsing?
JSON serializer settings can now be passed straight to DocumentClient constuctors, which allows for the flexibility you required.
Eventually found a workaround, since the JsonSettings are not accesible at this time, I'm using a JsonConverter:
public class StringJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return true;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override bool CanRead
{
get { return true; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.ValueType.Equals(typeof(DateTime)))
{
return ((DateTime)reader.Value).ToIso8601Date();
}
if (reader.ValueType.Equals(typeof(DateTimeOffset)))
{
return ((DateTimeOffset)reader.Value).DateTime.ToIso8601Date();
}
return (string)reader.Value;
}
}
With this simple extension:
public static class DateTimeExtensions
{
private const string DateTimeFormat = "{0}-{1}-{2}T{3}:{4}:{5}Z";
public static string ToIso8601Date(this DateTime date)
{
if (date.Equals(DateTime.MinValue))
{
return null;
}
return string.Format(
DateTimeFormat,
date.Year,
PadLeft(date.Month),
PadLeft(date.Day),
PadLeft(date.Hour),
PadLeft(date.Minute),
PadLeft(date.Second));
}
private static string PadLeft(int number)
{
if (number < 10)
{
return string.Format("0{0}", number);
}
return number.ToString(CultureInfo.InvariantCulture);
}
}

Serializing a custom subclass of NameValueCollection with Json.Net

I have the following class I am unsuccessfully attempting to serialize to Json.
class HL7 : NameValueCollection
{
public List<HL7> Children { get; set; }
public HL7()
{
Children = new List<HL7>();
}
}
I have created the object like so and added data to it:
HL7 hl7 = new HL7();
hl7.Add("a", "123");
hl7.Add("b", "456");
hl7.Children.Add(new HL7());
hl7.Children[0].Add("c", "123");
hl7.Children[0].Add("d", "456");
When I call
JsonConvert.SerializeObject(hl7)
I receive
["a","b"]
I was expecting the following:
{
"a": "123",
"b": "456",
"Children": [
{
"c": "123",
"d": "456",
}
]
}
There are a few things going on here:
Json.NET cannot serialize a NameValueCollection without a custom converter because NameValueCollection implements IEnumerable for iterating over the keys, but does not implement IDictionary for iterating over keys and values. See this answer for a fuller explanation of why this causes problems for Json.NET.
Because NameValueCollection implements IEnumerable, Json.NET sees your class as a collection, and so serializes it as a JSON array and not a JSON object with named properties. Thus, your Children are not serialized. Again, a custom converter would be required to fix this.
Assuming the above issues are resolved, if your HL7 subclass of NameValueCollection happens to have a key named "Children" you will generate invalid JSON when serializing it, namely an object with duplicated property names. I suggest moving the names & values into a nested property (named, e.g., "Values") for purposes of unambiguous serialization.
NameValueCollection actually can have multiple string values for a given key string, so its entry values need to be serialized as a JSON array not a single string.
Putting all this together, the following code:
[JsonConverter(typeof(HL7Converter))]
public class HL7 : NameValueCollection
{
public List<HL7> Children { get; set; }
public HL7()
{
Children = new List<HL7>();
}
}
public class HL7Converter : JsonConverter
{
class HL7Proxy
{
public NameValueCollectionDictionaryWrapper Values { get; set; }
public List<HL7> Children { get; set; }
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(HL7);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var proxy = serializer.Deserialize<HL7Proxy>(reader);
if (proxy == null)
return existingValue;
var hl7 = existingValue as HL7;
if (hl7 == null)
hl7 = new HL7();
hl7.Add(proxy.Values.GetCollection());
if (proxy.Children != null)
hl7.Children.AddRange(proxy.Children);
return hl7;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
HL7 hl7 = (HL7)value;
if (hl7 == null)
return;
serializer.Serialize(writer, new HL7Proxy { Children = hl7.Children, Values = new NameValueCollectionDictionaryWrapper(hl7) });
}
}
// Proxy dictionary to serialize & deserialize a NameValueCollection. We use a proxy dictionary rather than a real dictionary because NameValueCollection is an ordered collection but the generic dictionary class is unordered.
public class NameValueCollectionDictionaryWrapper: IDictionary<string, string []>
{
readonly NameValueCollection collection;
public NameValueCollectionDictionaryWrapper()
: this(new NameValueCollection())
{
}
public NameValueCollectionDictionaryWrapper(NameValueCollection collection)
{
this.collection = collection;
}
// Method instead of a property to guarantee that nobody tries to serialize it.
public NameValueCollection GetCollection()
{
return collection;
}
#region IDictionary<string,string[]> Members
public void Add(string key, string[] value)
{
if (collection.GetValues(key) != null)
throw new ArgumentException("Duplicate key " + key);
foreach (var str in value)
collection.Add(key, str);
}
public bool ContainsKey(string key)
{
return collection.GetValues(key) != null;
}
public ICollection<string> Keys
{
get {
return collection.AllKeys;
}
}
public bool Remove(string key)
{
bool found = ContainsKey(key);
if (found)
collection.Remove(key);
return found;
}
public bool TryGetValue(string key, out string[] value)
{
value = collection.GetValues(key);
return value != null;
}
public ICollection<string[]> Values
{
get {
return Enumerable.Range(0, collection.Count).Select(i => collection.GetValues(i)).ToArray();
}
}
public string[] this[string key]
{
get
{
var value = collection.GetValues(key);
if (value == null)
throw new KeyNotFoundException();
return value;
}
set
{
Remove(key);
Add(key, value);
}
}
#endregion
#region ICollection<KeyValuePair<string,string[]>> Members
public void Add(KeyValuePair<string, string[]> item)
{
Add(item.Key, item.Value);
}
public void Clear()
{
collection.Clear();
}
public bool Contains(KeyValuePair<string, string[]> item)
{
string [] value;
if (!TryGetValue(item.Key, out value))
return false;
return EqualityComparer<string[]>.Default.Equals(item.Value, value); // Consistent with Dictionary<TKey, TValue>
}
public void CopyTo(KeyValuePair<string, string[]>[] array, int arrayIndex)
{
foreach (var item in this)
array[arrayIndex++] = item;
}
public int Count
{
get { return collection.Count; }
}
public bool IsReadOnly
{
get { return false; }
}
public bool Remove(KeyValuePair<string, string[]> item)
{
if (Contains(item))
return Remove(item.Key);
return false;
}
#endregion
#region IEnumerable<KeyValuePair<string,string[]>> Members
public IEnumerator<KeyValuePair<string, string[]>> GetEnumerator()
{
foreach (string key in collection)
{
yield return new KeyValuePair<string, string[]>(key, collection.GetValues(key));
}
}
#endregion
#region IEnumerable Members
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
}
Using the following test case:
HL7 hl7 = new HL7();
hl7.Add("a", "123");
hl7.Add("b", "456");
hl7.Add("Children", "Children");
hl7.Children.Add(new HL7());
hl7.Children[0].Add("c", "123");
hl7.Children[0].Add("d", "456");
hl7.Children[0].Add("d", "789");
var json = JsonConvert.SerializeObject(hl7, Formatting.Indented);
Debug.WriteLine(json);
Gives the following JSON:
{
"Values": {
"a": [
"123"
],
"b": [
"456"
],
"Children": [
"Children"
]
},
"Children": [
{
"Values": {
"c": [
"123"
],
"d": [
"456",
"789"
]
},
"Children": []
}
]
}
Inspired by this answer
how to convert NameValueCollection to JSON string?
, here is the working code (the only bad part is probably the "Children" string that is the property name. If you'll do a refactor, this will cause an error.
JsonConvert.SerializeObject(NvcToDictionary(hl7, false));
And the function:
static Dictionary<string, object> NvcToDictionary(HL7 nvc, bool handleMultipleValuesPerKey)
{
var result = new Dictionary<string, object>();
foreach (string key in nvc.Keys)
{
if (handleMultipleValuesPerKey)
{
string[] values = nvc.GetValues(key);
if (values.Length == 1)
{
result.Add(key, values[0]);
}
else
{
result.Add(key, values);
}
}
else
{
result.Add(key, nvc[key]);
}
}
if (nvc.Children.Any())
{
var listOfChildrenDictionary = new List<Dictionary<string, object>>();
foreach (var nvcChildren in nvc.Children){
listOfChildrenDictionary.Add(NvcToDictionary(nvcChildren, false));
}
result.Add("Children", listOfChildrenDictionary);
}
return result;
}
I have had issues with serializing NameValueCollections, using JSON.Net, The only way I have found is to convert it to a dictionary and then serialize it like:
var jsonString = JsonConvert.SerializeObject(new
{
Parent = hl7.AllKeys.ToDictionary(r => r, r => hl7[r]),
Children = hl7.Children.Select(c => c.AllKeys.ToDictionary(sub => sub, sub => c[sub]))
}, Newtonsoft.Json.Formatting.Indented);
and you will end up with:
{
"Parent": {
"a": "123",
"b": "456"
},
"Children": [
{
"c": "123",
"d": "456"
}
]
}
But this will return "Parent" as well for top level items, since you have to specify a name for property in anonymous type
Here's a custom serializer that will write the JSON as you were looking for, example program attached. The serializer is at the bottom. Note that you will need to add this converter to the JSON serializer settings, either through the default as I've done, or through the constructor of your serializer. Alternately, since you have a subclass you can use the JsonConverterAttribute on the HL7 class to assign the serializer
public class Program
{
static int Main(string[] args) {
JsonConvert.DefaultSettings = () => new JsonSerializerSettings {
Converters = new []{ new HL7Converter() }
};
HL7 hl7 = new HL7();
hl7.Add("a", "123");
hl7.Add("b", "456");
hl7.Children.Add(new HL7());
hl7.Children[0].Add("c", "123");
hl7.Children[0].Add("d", "456");
Console.WriteLine (JsonConvert.SerializeObject (hl7));
return 0;
}
}
public class HL7 : NameValueCollection
{
public List<HL7> Children { get; set; }
public HL7()
{
Children = new List<HL7> ();
}
}
public class HL7Converter : Newtonsoft.Json.JsonConverter {
#region implemented abstract members of JsonConverter
public override void WriteJson (Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
var collection = (HL7)value;
writer.WriteStartObject ();
foreach (var key in collection.AllKeys) {
writer.WritePropertyName (key);
writer.WriteValue (collection [key]);
}
writer.WritePropertyName ("Children");
serializer.Serialize (writer,collection.Children);
writer.WriteEndObject ();
}
public override object ReadJson (Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
HL7 collection = existingValue as HL7 ?? new HL7 ();
JObject jObj = JObject.Load (reader);
foreach (var prop in jObj.Properties()) {
if (prop.Name != "Children") {
collection.Add (prop.Name, prop.Value.ToObject<string> ());
} else {
collection.Children = jObj.ToObject<List<HL7>> ();
}
}
return collection;
}
public override bool CanConvert (Type objectType)
{
return objectType == typeof(HL7);
}
#endregion
}

Handling wrong contract type in JSON.NET

Is it possible to skip a part of json when contract type does not match. Namely, I receive a json response which does not conform the schema - null objects are serialized as empty arrays ("thank's" to php serializer). So, instead of "null" or "{}" I get "[]".
So far, I tried to use Error event handler available in JsonSerializerSettings. However, this approach is rather ugly because Json.net does not return meaningful exception type.
Example of valid response:
{
"key": "key",
"id": "id",
"status": "status",
"opts": {
"start": 42,
"limit": 12
}
}
}
Example of invalid response
{
"key": "key",
"id": "id",
"status": "status",
"opts": []
}
You can handle this issue using a custom JsonConverter. The converter can detect the format of the opts property in the JSON and then either deserialize it or set its value to null in the target object. Here's how I would write the converter:
class OptsConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(Opts));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
if (token.Type == JTokenType.Object)
{
return new Opts
{
start = (int)token["start"],
limit = (int)token["limit"]
};
}
return null;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Here is a demo:
class Program
{
static void Main(string[] args)
{
string valid = #"
{
""key"": ""key"",
""id"": ""id"",
""status"": ""status"",
""opts"": {
""start"": 42,
""limit"": 12
}
}";
DeserializeAndWriteToConsole(valid);
string invalid = #"
{
""key"": ""key"",
""id"": ""id"",
""status"": ""status"",
""opts"": []
}";
DeserializeAndWriteToConsole(invalid);
}
private static void DeserializeAndWriteToConsole(string json)
{
RootObject root = JsonConvert.DeserializeObject<RootObject>(json, new OptsConverter());
Console.WriteLine("key: " + root.key);
Console.WriteLine("id: " + root.id);
Console.WriteLine("status: " + root.status);
if (root.opts != null)
{
Console.WriteLine("opts.start: " + root.opts.start);
Console.WriteLine("opts.limit: " + root.opts.limit);
}
else
{
Console.WriteLine("opts: (null)");
}
Console.WriteLine();
}
}
public class RootObject
{
public string key { get; set; }
public string id { get; set; }
public string status { get; set; }
public Opts opts { get; set; }
}
public class Opts
{
public int start { get; set; }
public int limit { get; set; }
}
Output:
key: key
id: id
status: status
opts.start: 42
opts.limit: 12
key: key
id: id
status: status
opts: (null)

Resources