How to serialize/deserialize a custom collection with additional properties using Json.Net
The problem is the following: when an object implements IEnumerable
, JSON.net identifies it as an array of values and serializes it following the array Json syntax (that does not include properties),
e.g. :
[ {"FooProperty" : 123}, {"FooProperty" : 456}, {"FooProperty" : 789}]
If you want to serialize it keeping the properties, you need to handle the serialization of that object by hand by defining a custom JsonConverter
:
// intermediate class that can be serialized by JSON.net
// and contains the same data as FooCollection
class FooCollectionSurrogate
{
// the collection of foo elements
public List<Foo> Collection { get; set; }
// the properties of FooCollection to serialize
public string Bar { get; set; }
}
public class FooCollectionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(FooCollection);
}
public override object ReadJson(
JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
// N.B. null handling is missing
var surrogate = serializer.Deserialize<FooCollectionSurrogate>(reader);
var fooElements = surrogate.Collection;
var fooColl = new FooCollection { Bar = surrogate.Bar };
foreach (var el in fooElements)
fooColl.Add(el);
return fooColl;
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
// N.B. null handling is missing
var fooColl = (FooCollection)value;
// create the surrogate and serialize it instead
// of the collection itself
var surrogate = new FooCollectionSurrogate()
{
Collection = fooColl.ToList(),
Bar = fooColl.Bar
};
serializer.Serialize(writer, surrogate);
}
}
Then use it as follows:
var ss = JsonConvert.SerializeObject(collection, new FooCollectionConverter());
var obj = JsonConvert.DeserializeObject<FooCollection>(ss, new FooCollectionConverter());
Serialise custom collection With additional properties and on Add to hookup item propertChanged events
You could serialize your custom container as a JsonObject
, as you are doing now, and serialize the embedded list as a proxy ObservableCollection<T>
. You can then listen in for additions and removals to the proxy and handle them accordingly. Note -- no custom JsonConverter required. Since I don't have your definition for ITrackableEntity
, here's a quick prototype wrapper IList<T>
for a List<T>
:
[Serializable]
[JsonObject]
public class ListContainer<T> : IList<T>
{
[JsonIgnore]
readonly List<T> _list = new List<T>();
[JsonProperty("List")]
private IList<T> SerializableList
{
get
{
var proxy = new ObservableCollection<T>(_list);
proxy.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(proxy_CollectionChanged);
return proxy;
}
set
{
_list.Clear();
_list.AddRange(value);
}
}
void proxy_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
foreach (var item in e.NewItems.Cast<T>())
Add(item);
}
else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
{
foreach (var item in e.NewItems.Cast<T>())
Remove(item);
}
else
{
Debug.Assert(false);
throw new NotImplementedException();
}
}
[JsonIgnore]
public int Count
{
get { return _list.Count; }
}
[JsonIgnore]
public bool IsReadOnly
{
get { return ((IList<T>)_list).IsReadOnly; }
}
// Everything beyond here is boilerplate.
#region IList<T> Members
public int IndexOf(T item)
{
return _list.IndexOf(item);
}
public void Insert(int index, T item)
{
_list.Insert(index, item);
}
public void RemoveAt(int index)
{
_list.RemoveAt(index);
}
public T this[int index]
{
get
{
return _list[index];
}
set
{
_list[index] = value;
}
}
#endregion
#region ICollection<T> Members
public void Add(T item)
{
_list.Add(item);
}
public void Clear()
{
_list.Clear();
}
public bool Contains(T item)
{
return _list.Contains(item);
}
public void CopyTo(T[] array, int arrayIndex)
{
_list.CopyTo(array, arrayIndex);
}
public bool Remove(T item)
{
return _list.Remove(item);
}
#endregion
#region IEnumerable<T> Members
public IEnumerator<T> GetEnumerator()
{
return _list.GetEnumerator();
}
#endregion
#region IEnumerable Members
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
}
And then, to test:
public static void TestListContainerJson()
{
var list = new ListContainer<int>();
list.Add(101);
list.Add(102);
list.Add(103);
var json = JsonConvert.SerializeObject(list);
var newList = JsonConvert.DeserializeObject<ListContainer<int>>(json);
Debug.Assert(list.SequenceEqual(newList)); // No assert.
}
Update
It turns out that Json.NET follows the same pattern as XmlSerializer
: if you serialize the proxy list as an array, the setter will be called with the fully populated array after being read, and you can add them as required:
[Serializable]
[JsonObject]
public class ListContainer<T> : IList<T>
{
[JsonIgnore]
readonly List<T> _list = new List<T>();
[JsonProperty("List")]
private T [] SerializableList
{
get
{
return _list.ToArray();
}
set
{
Clear();
foreach (var item in value)
Add(item);
}
}
[JsonIgnore]
public int Count
{
get { return _list.Count; }
}
[JsonIgnore]
public bool IsReadOnly
{
get { return ((IList<T>)_list).IsReadOnly; }
}
// Everything beyond here is boilerplate.
}
This is much cleaner than my first solution.
Also, I suspect that your NewItems
and ModifiedItems
list contain references to items in the main _list
. By default Json.NET will effectively clone these during serialization & deserialization. To avoid this, look into the PreserveReferencesHandling
functionality. More here.
How can I make Json.NET serialize and deserialize declared properties of custom dynamic types that also implement IDictionary string, object ?
You have a few problems here:
You need to correctly override
DynamicObject.GetDynamicMemberNames()
as explained in this answer to Serialize instance of a class deriving from DynamicObject class by AlbertK
for Json.NET to be able to serialize your dynamic properties.(This has already been fixed in the edited version of your question.)
Declared properties do not show up unless you explicitly mark them with
[JsonProperty]
(as explained in this answer to C# How to serialize (JSON, XML) normal properties on a class that inherits from DynamicObject) but your type definitions are read-only and cannot be modified.The problem here seems to be that
JsonSerializerInternalWriter.SerializeDynamic()
only serializes declared properties for whichJsonProperty.HasMemberAttribute == true
. (I don't know why this check is made there, it would seem to make more sense to setCanRead
orIgnored
inside the contract resolver.)You would like for your class to implement
IDictionary<string, object>
, but if you do, it breaks deserialization; declared properties are no longer populated, but are instead added to the dictionary.The problem here seems to be that
DefaultContractResolver.CreateContract()
returnsJsonDictionaryContract
rather thanJsonDynamicContract
when the incoming type implementsIDictionary<TKey, TValue>
for anyTKey
andTValue
.
Assuming you have fixed issue #1, issues #2 and #3 can be handled by using a custom contract resolver such as the following:
public class MyContractResolver : DefaultContractResolver
{
protected override JsonContract CreateContract(Type objectType)
{
// Prefer JsonDynamicContract for MyDynamicObject
if (typeof(MyDynamicObject).IsAssignableFrom(objectType))
{
return CreateDynamicContract(objectType);
}
return base.CreateContract(objectType);
}
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
var properties = base.CreateProperties(type, memberSerialization);
// If object type is a subclass of MyDynamicObject and the property is declared
// in a subclass of MyDynamicObject, assume it is marked with JsonProperty
// (unless it is explicitly ignored). By checking IsSubclassOf we ensure that
// "bookkeeping" properties like Count, Keys and Values are not serialized.
if (type.IsSubclassOf(typeof(MyDynamicObject)) && memberSerialization == MemberSerialization.OptOut)
{
foreach (var property in properties)
{
if (!property.Ignored && property.DeclaringType.IsSubclassOf(typeof(MyDynamicObject)))
{
property.HasMemberAttribute = true;
}
}
}
return properties;
}
}
Then, to use the contract resolver, cache it somewhere for performance:
static IContractResolver resolver = new MyContractResolver();
And then do:
var settings = new JsonSerializerSettings
{
ContractResolver = resolver,
};
string json = JsonConvert.SerializeObject(obj, settings);
Sample fiddle here.
How to serialize/deserialize an ArrayList and properties that are of an Object type
I decided to use soap serialization instead as it adds the types into the xml
using System;
using System.Text;
using System.IO;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Serialization.Formatters.Soap;
namespace Utils
{
public class XMLUtil
{
public static Byte[] StringToUTF8ByteArray(String xmlString)
{
return new UTF8Encoding().GetBytes(xmlString);
}
public static String SerializeToXML<T>(T objectToSerialize)
{
using (MemoryStream ms = new MemoryStream())
using (StreamWriter sw = new StreamWriter(ms, Encoding.UTF8))
{
SoapFormatter soapFormatter = new SoapFormatter();
soapFormatter.AssemblyFormat = FormatterAssemblyStyle.Simple;
soapFormatter.Serialize(ms, objectToSerialize);
String decoded = Encoding.UTF8.GetString(ms.ToArray());
return decoded;
}
}
public static T DeserializeFromXML<T>(string xmlString) where T : class
{
T retval = default(T);
using (MemoryStream stream = new MemoryStream(StringToUTF8ByteArray(xmlString)))
{
SoapFormatter soapFormatter = new SoapFormatter();
soapFormatter.AssemblyFormat = FormatterAssemblyStyle.Simple;
retval = soapFormatter.Deserialize(stream) as T;
}
return retval;
}
}
}
Newtonsoft Jsonconverter not serializing properties when derived of list
Don't derive you class from List<>
as mentioned here.
Change your class to:
public class Plep
{
public string Name { get; set; } = "smew";
public List<Subs> Subs {get;set;}
public void SaveToFile(string file)
{
using (StreamWriter wrt = new StreamWriter(file))
{
wrt.WriteLine(JsonConvert.SerializeObject(this, Formatting.Indented, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.All,
//TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
}));
}
}
Related Topics
Find a Control in Windows Forms by Name
Memory Allocation: Stack VS Heap
What Does Missingmanifestresourceexception Mean and How to Fix It
Parallel.Foreach Slower Than Foreach
How to Get Client Date and Time in ASP.NET
Cannot Step into .Net Framework Source Code
ASP.NET MVC Ambiguous Action Methods
How to Create Custom Config Section in App.Config
Can a C# Class Inherit Attributes from Its Interface
Deserialize a JSON Array in C#
The Name 'Configurationmanager' Does Not Exist in the Current Context
Differencebetween Directory.Enumeratefiles VS Directory.Getfiles
Does Parallel.Foreach Limit the Number of Active Threads
What Does "Datetime" Mean in C#
Newtonsoft.JSON Cannot Convert Model with Typeconverter Attribute
Websocket Server: Onopen Function on the Web Socket Is Never Called