Rather new to Json.net and tried the following simple example serializing and then deserialing an object getting the error below:
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Collections;
namespace Timehunter.Base.ServicesTests
{
/// <summary>
/// Summary description for JsonError
/// </summary>
[TestClass]
public class JsonError
{
[TestMethod]
public void TestMethod1()
{
JsonSerializerSettings serializerSettings = new JsonSerializerSettings()
{
DateFormatHandling = DateFormatHandling.IsoDateFormat,
DateParseHandling = Newtonsoft.Json.DateParseHandling.DateTimeOffset
};
Act.Activities acts = new Act.Activities();
acts.Add(new Act.Activity() { Id = 1, Name = "test1" });
acts.Add(new Act.Activity() { Id = 2, Name = "test2" });
string json = Newtonsoft.Json.JsonConvert.SerializeObject(acts, serializerSettings);
Timehunter.Base.Act.Activities target = Newtonsoft.Json.JsonConvert.DeserializeObject<Timehunter.Base.Act.Activities>(json, serializerSettings);
Assert.AreEqual("test1", target.List[0].Name, "Name of first activity");
}
}
}
namespace Timehunter.Base
{
[DataContract]
public class Activity
{
private int _id;
private string _name;
[DataMember]
public int Id
{
get { return this._id; }
set { this._id = value; }
}
[DataMember]
public string Name
{
get { return this._name; }
set { this._name = value; }
}
public Activity()
{
this._id = new int();
this._name = string.Empty;
}
}
[DataContract]
public class Activities : IEnumerable<Activity>
{
private List<Activity> _list;
[DataMember]
public List<Activity> List
{
get { return this._list; }
set { this._list = value; }
}
public Activities()
{
this._list = new List<Activity>();
}
public void Add(Activity item)
{ this._list.Add(item); }
public bool Remove(Activity item)
{ return this._list.Remove(item); }
public int Count()
{ return this._list.Count; }
public IEnumerator<Activity> GetEnumerator()
{
return this._list.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
And then I get the following error:
Test Name: TestMethod1
Test FullName: Timehunter.Base.ServicesTests.JsonError.TestMethod1
Test Source: C:\Users\hawi.HAWCONS\Documents\Visual Studio 2015\Projects\Timehunter.Data\Timehunter.Base.ServicesTests\JsonError.cs : line 67
Test Outcome: Failed
Test Duration: 0:00:00,2038359
Result StackTrace:
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateNewList(JsonReader reader, JsonArrayContract contract, Boolean& createdFromNonDefaultCreator)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateList(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, Object existingValue, String id)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.CreateValueInternal(JsonReader reader, Type objectType, JsonContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerMember, Object existingValue)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.Deserialize(JsonReader reader, Type objectType, Boolean checkAdditionalContent)
at Newtonsoft.Json.JsonSerializer.DeserializeInternal(JsonReader reader, Type objectType)
at Newtonsoft.Json.JsonConvert.DeserializeObject(String value, Type type, JsonSerializerSettings settings)
at Newtonsoft.Json.JsonConvert.DeserializeObject[T](String value, JsonSerializerSettings settings)
at Timehunter.Base.ServicesTests.JsonError.TestMethod1() in C:\Users\hawi.HAWCONS\Documents\Visual Studio 2015\Projects\Timehunter.Data\Timehunter.Base.ServicesTests\JsonError.cs:line 79
Result Message:
Test method Timehunter.Base.ServicesTests.JsonError.TestMethod1 threw exception:
Newtonsoft.Json.JsonSerializationException: Cannot create and populate list type Timehunter.Base.Act.Activities. Path '', line 1, position 1.
What am I doing wrong?
Update 2
This is getting reverted back in 11.0.2 for backwards compatibility. Refer to the original answer for a solution.
Update
Reported as Issue #1598: DataContractAttribute does not cause JSon object serialization for IEnumerable and fixed in commit e9e2d00
. It should be in the next release after 10.0.3 which will probably be Json.NET version 11.
Original answer
I notice you have marked your Activities
class with [DataContract]
and [DataMember]
:
[DataContract]
public class Activities : IEnumerable<Activity>
{
private List<Activity> _list;
[DataMember]
public List<Activity> List
{
get { return this._list; }
set { this._list = value; }
}
// ...
}
Applying [DataContact]
will cause DataContractJsonSerializer
to serialize an IEnumerable<T>
as a JSON object with properties, rather than as a JSON array. Since Json.NET supports data contract attributes when applied to non-enumerables, you might be thinking that it will respect them on enumerables and collections as well.
However, it appears this is not implemented. If I serialize your class with DataContractJsonSerializer
, I see
{"List":[{"Id":1,"Name":"test1"},{"Id":2,"Name":"test2"}]}
But if I serialize with Json.NET, I see that the [DataContract]
was ignored:
[{"Id":1,"Name":"test1"},{"Id":2,"Name":"test2"}]
Then later it throws an exception during deserialization because it doesn't know how to add members to your IEnumerable<Activity>
class. (It would have been able to add members if your class implemented ICollection<Activity>
, or had a constructor with an IEnumerable<Activity>
argument.)
So, should this work? The documentation page Serialization Attributes states:
The DataContractAttribute can be used as substitute for JsonObjectAttribute. The DataContractAttribute will default member serialization to opt-in.
Which implies that Json.NET ought to work the way you expect. You could report an issue about it if you want -- at least the documentation should be clarified.
As a workaround, if you want to force Json.NET to serialize a collection as an object, you need to use [JsonObject]
instead:
[DataContract]
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public class Activities : IEnumerable<Activity>
{
private List<Activity> _list;
[DataMember]
[JsonProperty]
public List<Activity> List
{
get { return this._list; }
set { this._list = value; }
}
// Remainder unchanged.
}
If you have many enumerable classes with [DataContract]
applied, or cannot add a dependency on Json.NET to your models, you can create a custom ContractResolver
that checks for the presence of [DataContract]
on enumerable classes and serializes them as objects:
public class DataContractForCollectionsResolver : DefaultContractResolver
{
// As of 7.0.1, Json.NET suggests using a static instance for "stateless" contract resolvers, for performance reasons.
// http://www.newtonsoft.com/json/help/html/ContractResolver.htm
// http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_DefaultContractResolver__ctor_1.htm
// "Use the parameterless constructor and cache instances of the contract resolver within your application for optimal performance."
static DataContractForCollectionsResolver instance;
static DataContractForCollectionsResolver() { instance = new DataContractForCollectionsResolver(); }
public static DataContractForCollectionsResolver Instance { get { return instance; } }
protected DataContractForCollectionsResolver() : base() { }
protected override JsonContract CreateContract(Type objectType)
{
var t = (Nullable.GetUnderlyingType(objectType) ?? objectType);
if (!t.IsPrimitive
&& t != typeof(string)
&& !t.IsArray
&& typeof(IEnumerable).IsAssignableFrom(t)
&& !t.GetCustomAttributes(typeof(JsonContainerAttribute),true).Any())
{
if (t.GetCustomAttributes(typeof(DataContractAttribute),true).Any())
return base.CreateObjectContract(objectType);
}
return base.CreateContract(objectType);
}
}
Then use the following settings:
var serializerSettings = new JsonSerializerSettings()
{
DateFormatHandling = DateFormatHandling.IsoDateFormat,
DateParseHandling = Newtonsoft.Json.DateParseHandling.DateTimeOffset,
ContractResolver = DataContractForCollectionsResolver.Instance
};