I am looking for a way to split a JSON string into multiple lines after serialization, inserting a newline after every Nth property.
For example, I have this class:
public class Obj
{
public string Property1 { get; set; }
public string Property2 { get; set; }
public string[] Property3 { get; set; }
public string Property4 { get; set; }
public string Property5 { get; set; }
public string Property6 { get; set; }
public TimeSpan Time { get; set; }
public string Property7 { get; set; }
public string Property8 { get; set; }
public string Property9 { get; set; }
public string Property10 { get; set; }
public string[] Property11 { get; set; }
}
Initialized to something like this:
var root = new Obj
{
Property1 = "value1",
Property2 = "value2",
Property3 = new[] {"test", "test1", "test3"},
Property4 = "value4",
Property5 = "value5",
Property6 = "value6",
Time = TimeSpan.FromSeconds(13),
Property7 = "value7",
Property8 = "value8",
Property9 = "value9",
Property10 = "value10",
Property11 = new string[] {"asdf", "basdf"}
};
When I call JsonConvert.SerializeObject(root), it prints out:
{"Property1":"value1","Property2":"value2","Property3":["test","test1","test3"],"Property4":"value4","Property5":"value5","Property6":"value6","Time":"00:00:13","Property7":"value7","Property8":"value8","Property9":"value9","Property10":"value10","Property11":["asdf","basdf"]}
I would like to include a new line once after every Nth property. Let's say every 3rd property.
I tried something like this:
public static string ReplaceEveryNth(string fullString, string pattern, int n)
{
if (n < 1) { return fullString; }
var index = 1;
return Regex.Replace(fullString, pattern, m =>
{
return (index++ % n == 0) ? m.Value + Environment.NewLine : m.Value;
});
}
and called it like this to match key-value pairs on the JSON string:
var replaced = ReplaceEveryNth(json, "(\".*?\":\".*?\"),", 3);
Now, this works for simple properties. But when I start introducing types like arrays, the Regex becomes more complex.
I am wondering if there is a simpler approach.
I'm not sure if this is exactly what you are looking for, but you could try implementing a custom JsonWriter to insert a newline before every Nth property name (assuming you are using Json.Net):
public class CustomJsonWriter : JsonTextWriter
{
public int N { get; set; }
private int propertyCount = 0;
public CustomJsonWriter(TextWriter textWriter, int n) : base(textWriter)
{
N = n;
}
public override void WritePropertyName(string name, bool escape)
{
if (propertyCount > 0 && propertyCount % N == 0)
WriteWhitespace(Environment.NewLine);
base.WritePropertyName(name, escape);
propertyCount++;
}
}
A helper method will make it easy to use:
public static string SerializeWithCustomFormatting(object obj, int n)
{
using (TextWriter sw = new StringWriter())
using (JsonWriter writer = new CustomJsonWriter(sw, n))
{
JsonSerializer ser = new JsonSerializer();
ser.Serialize(writer, obj);
return sw.ToString();
}
}
Then you can do:
string json = SerializeWithCustomFormatting(root, 3);
With your example it would produce output like this:
{"Property1":"value1","Property2":"value2","Property3":["test","test1","test3"]
,"Property4":"value4","Property5":"value5","Property6":"value6"
,"Time":"00:00:13","Property7":"value7","Property8":"value8"
,"Property9":"value9","Property10":"value10","Property11":["asdf","basdf"]}
Fiddle: https://dotnetfiddle.net/gG8az2
Would having a fully indented json work for you?
var json = JsonConvert.SerializeObject(o, new JsonSerializerSettings
{
Formatting = Formatting.Indented
});
Update
Since fully indented json is not good enough you could try a custom converter.
Result
{"Property1":"value1","Property2":"value2","Property3":["test","test1","test3"]
,"Property4":"value4","Property5":"value5","Property6":"value6"
,"Time":"00:00:13","Property7":"value7","Property8":"value8"
,"Property9":"value9","Property10":"value10","Property11":["asdf","basdf"]
}
Converter
public class CustomLineBreakerConverter : JsonConverter
{
private readonly uint n;
private uint i = 1;
public CustomLineBreakerConverter(uint n) { this.n = n; }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// Scaffolding from https://www.newtonsoft.com/json/help/html/CustomJsonConverter.htm
// Please note this will not work recursively (only the top level will be have the new lines
JToken t = JToken.FromObject(value);
if (t.Type != JTokenType.Object)
{
t.WriteTo(writer);
}
else
{
JObject o = (JObject)t;
var properties = o.Properties();
writer.WriteStartObject();
foreach( var p in properties)
{
p.WriteTo(writer);
if (i++ % n == 0)
{
writer.WriteWhitespace("\r\n"); // This will write a new line after the property even if no more properties
}
}
writer.WriteEndObject();
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
=> throw new NotImplementedException("This converter is meant only for writing");
public override bool CanConvert(Type objectType) => true;
}
Test code
var o = new
{
Property1 = "value1",
Property2 = "value2",
Property3 = new[] { "test", "test1", "test3" },
Property4 = "value4",
Property5 = "value5",
Property6 = "value6",
Time = TimeSpan.FromSeconds(13),
Property7 = "value7",
Property8 = "value8",
Property9 = "value9",
Property10 = "value10",
Property11 = new string[] { "asdf", "basdf" }
};
var json = JsonConvert.SerializeObject(o, new JsonSerializerSettings
{
Formatting = Formatting.None,
Converters = new List<JsonConverter>() { new CustomLineBreakerConverter(3) }
});
Console.WriteLine(json);
Related
How can I set JsonSerializer to not add "\u0022" to string for EventData property? Because I get:
{"Id":5,"CreateDate":"2021-04-21T05:26:30.9817284Z","EventData":"{\u0022Id\u0022:1,\u0022Email\u0022:\u0022test#test.test\u0022}"}
I will never deserialize EventData, it must be readable. And I want:
{"Id":5,"CreateDate":"2021-04-21T05:26:30.9817284Z","EventData":"{Id:1,Email:test#test.test}"}
My code:
public class EmailSent
{
public int Id { get; set; }
public string Email { get; set; }
}
public class UserCreated
{
public int Id { get; set; }
public DateTime CreateDate { get; set; }
public string EventData { get; set; }
}
var emailSent = new EmailSent
{
Id = 1,
Email = "test#test.test"
};
var userCreated = new UserCreated
{
Id = 5,
CreateDate = DateTime.UtcNow,
EventData = JsonSerializer.Serialize(emailSent) // I will never deserialize it
};
string result = JsonSerializer.Serialize(userCreated);
You can use, for example, UnsafeRelaxedJsonEscaping:
var serializeOptions = new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
string json = JsonSerializer.Serialize(userCreated, serializeOptions);
This will produce the following output:
{
"Id": 5,
"CreateDate": "2021-04-21T07:49:23.4378969Z",
"EventData": "{\"Id\":1,\"Email\":\"test#test.test\"}"
}
Reference: How to customize character encoding with System.Text.Json. Please read the caution there:
Caution
Compared to the default encoder, the UnsafeRelaxedJsonEscaping encoder is more permissive about allowing characters to pass through unescaped:
(...)
This happens because JsonSerializer.Serialize() is invoked more than once.
You can specify 'JSONSerializerOptions' Encoder equal to JavaScriptTenCoder. Create(new TextEncoderSettings(UnicodeRanges.All))
like this
public static string ToString(this object str)
{
try
{
string sResult = JsonSerializer.Serialize(str, new JsonSerializerOptions{WriteIndented = true, Encoder = JavaScriptEncoder.Create(new TextEncoderSettings(System.Text.Unicode.UnicodeRanges.All))});
return sResult;
}
}
I have almost the exact same problem, only my UserCreated is being returned by ASP.NET controller function, I don't directly control serialization.
The simplest solution is to change the type of EventData to JsonNode.
To set EventData from whatever complex object you want, you can use this awkward construct
EventData = JsonNode.Parse(JsonSerializer.Serialize(complexData))
Complete minimal example:
[Test]
public void Test1()
{
Dictionary<string, string> complexData =
new Dictionary<string, string>() { { "A", "B" } };
NestedJason toSerialize = new NestedJason()
{
FormName = "String",
FormData = JsonNode.Parse(JsonSerializer.Serialize(complexData))
};
Console.WriteLine(JsonSerializer.Serialize(toSerialize));
// prints {"FormName":"String","FormData":{"A":"B"}}
}
}
public class NestedJason
{
public string FormName { get; set; }
public JsonNode FormData { get; set; }
}
A cleaner option is to make FormData type object. (I'm assuming you don't want to make it type EmailSent because you want to store random data about many different types of events.) If you round trip the EventData as object, it will go in as EmailSent and come back out as JsonElement. Manual steps would be required to get it back to an EmailSent.
public class Tests
{
[Test]
public void Test1()
{
Dictionary<string, string> complexData =
new Dictionary<string, string>() { { "A", "B" } };
NestedJason toSerialize = new NestedJason()
{
FormName = "Sting",
FormData = complexData
};
Console.WriteLine(JsonSerializer.Serialize(toSerialize));
NestedJason result = JsonSerializer.Deserialize(
JsonSerializer.Serialize(toSerialize),
typeof(NestedJason)) as NestedJason;
Console.WriteLine(result.FormData.GetType().Name);
}
}
public class NestedJason
{
public string FormName { get; set; }
public Object FormData { get; set; }
}
(I personally will be using the previous JsonNode option, that is more editable, but the object option skips that round trip serialization kludge.)
I am trying to create an object structure like this
Universe - contains a collection of Worlds
Worlds contains a collection of areas.
All 3 of these types extend from a base class which has a property of 'ParentObject'.
Worlds will reference the Universe via this property.
Areas will reference the world via this property.
When running a test harness application.
Universe universe = Universe.GetInstance();
var world = universe.CreateWorld();
var area = world.CreateArea();
area.UpdateIpfs();
UpdateIpfs on the area object - then recurses upwards from area to world to universe - serializing each tier and then adding to the Ipfs network. (Thats just where I am storing the json data)
In order to get the objects back
I do
Universe universe = Universe.GetInstance("QmZnaSaDNnmqhUrE8kFHeu9PGGStAr2D4q3Vt88yHwFvzG");
var world = universe.Worlds[0];
var area = world.Areas[0];
Stepping through the code I can see the json content is this before deserialization:
{
"$id": "1",
"ParentObject": null,
"Worlds": [
{
"$id": "2",
"ParentObject": {
"$ref": "1"
},
"Time": "2019-10-06T23:13:56.6002056+01:00",
"Name": null,
"Description": null,
"Areas": [
{
"$id": "3",
"EventScripts": {
"$id": "4"
},
"ParentObject": {
"$ref": "2"
},
"Name": null,
"IsInterior": false,
"IsUnderground": false,
"IsArtificial": false,
"Description": null,
"X": 0,
"Y": 0,
"Z": 0,
"AreaObjects": [],
"ObjType": "BloodlinesLib.Engine.World.Area, BloodlinesLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"Hash": "QmbM86GZn6w9JadBM143fDYwGisgNPGXke3bFxpXzrfgJh"
}
],
"ObjType": "BloodlinesLib.Engine.World.World, BloodlinesLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"Hash": "QmRnEfndUDjCTifY694YuftPNWSRHQQJn9WwZmSpUBRJmv"
}
],
"ObjType": "BloodlinesLib.Engine.World.Universe, BloodlinesLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"Hash": null
}
I can see that the ParentObject for the area object is $ref: 2
Which seems to match the Id of the World object.
But when I deserialize - the ParentObject on the 'Area' object is equal to Null.
However the ParentObject on World object is correctly assigned.
Anyone have any ideas why my ParentObject is not being deserialized properly for the Area's ParentObject.
My serialization and deserialize code is here:
public void UpdateIpfs()
{
JsonSerializerSettings sets = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ContractResolver = new NonPublicPropertiesResolver()
};
var obj = Convert.ChangeType(this, ObjType);
if(ObjType == typeof(Universe))
{
Hash = null;
}
var strContent = JsonConvert.SerializeObject(obj, Formatting.Indented, sets);
var ipfs = new IpfsClient();
byte[] data = Encoding.UTF8.GetBytes(strContent);
data = CompressionHelper.Compress(data);
using (MemoryStream ms = new MemoryStream(data))
{
Hash = ipfs.FileSystem.AddAsync(ms).Result.Id.Hash.ToString();
}
if(ParentObject != null)
{
ParentObject.UpdateIpfs();
}
OnUpdate?.Invoke(this);
}
and
public static T LoadFromIpfs<T>(string hash)
{
JsonSerializerSettings sets = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects,
ContractResolver = new NonPublicPropertiesResolver()
};
var ipfs = new IpfsClient();
byte[] data;
using (MemoryStream ms = new MemoryStream())
{
ipfs.FileSystem.ReadFileAsync(hash).Result.CopyTo(ms);
data =ms.ToArray();
}
data = CompressionHelper.Decompress(data);
string content = Encoding.UTF8.GetString(data);
T obj = JsonConvert.DeserializeObject<T>(content, sets);
return obj;
}
Universe.cs
public class IpfsObject
{
public IpfsObject ParentObject;
public Type ObjType { get; set; }
public string Hash { get; set; }
public static T LoadFromIpfs<T>(string hash)
{
JsonSerializerSettings sets = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects
};
var ipfs = new IpfsClient();
byte[] data;
using (MemoryStream ms = new MemoryStream())
{
ipfs.FileSystem.ReadFileAsync(hash).Result.CopyTo(ms);
data =ms.ToArray();
}
data = CompressionHelper.Decompress(data);
string content = Encoding.UTF8.GetString(data);
T obj = JsonConvert.DeserializeObject<T>(content, sets);
return obj;
}
public delegate void OnUpdateDelegate(object obj);
public event OnUpdateDelegate OnUpdate;
public void UpdateIpfs()
{
JsonSerializerSettings sets = new JsonSerializerSettings
{
PreserveReferencesHandling = PreserveReferencesHandling.Objects
};
var obj = Convert.ChangeType(this, ObjType);
if(ObjType == typeof(Universe))
{
Hash = null;
}
var strContent = JsonConvert.SerializeObject(obj, Formatting.Indented, sets);
var ipfs = new IpfsClient();
byte[] data = Encoding.UTF8.GetBytes(strContent);
data = CompressionHelper.Compress(data);
using (MemoryStream ms = new MemoryStream(data))
{
Hash = ipfs.FileSystem.AddAsync(ms).Result.Id.Hash.ToString();
}
if(ParentObject != null)
{
ParentObject.UpdateIpfs();
}
OnUpdate?.Invoke(this);
}
}
public class Universe : IpfsObject
{
public Universe()
{
Worlds = new List<World>();
this.ObjType = typeof(Universe);
OnUpdate += Universe_OnUpdate;
}
private void Universe_OnUpdate(object obj)
{
IpfsObject ipfsObj = obj as IpfsObject;
Console.WriteLine("Universe updated: "+ ipfsObj.Hash);
File.WriteAllText("UniverseHash.txt", ipfsObj.Hash);
}
public World CreateWorld()
{
World world = new World(this);
Worlds.Add(world);
return world;
}
public List<World> Worlds { get; set; }
public static Universe GetInstance(string hash = null)
{
if(_universe == null)
{
if(hash == null)
{
_universe = new Universe();
}
else
{
_universe = IpfsObject.LoadFromIpfs<Universe>(hash);
_universe.Hash = hash;
}
}
return _universe;
}
private static Universe _universe;
}
public class World : Ipfs.IpfsObject
{
public World(Universe universe)
{
Time = DateTime.Now;
ParentObject = universe;
this.ObjType = typeof(World);
Areas = new List<Area>();
}
public Area CreateArea(bool findFreespace = false)
{
Area area = new Area(this);
Areas.Add(area);
if (findFreespace)
{
bool b = false;
Random rand = new Random(Guid.NewGuid().GetHashCode());
while (!b)
{
b = area.SetPosition(rand.Next(-500, 500), rand.Next(-500, 500), 0);
}
}
area.UpdateIpfs();
return area;
}
public void SetName(string name)
{
Name = name;
UpdateIpfs();
}
public void SetDescription(string desc)
{
Description = desc;
UpdateIpfs();
}
public DateTime Time { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<Area> Areas { get; set; }
}
public class Area : IpfsObject
{
public Area(World worldParent)
{
this.ObjType = typeof(Area);
ParentObject = worldParent;
AreaObjects = new List<AreaObject>();
}
public string Name { get; private set; }
public bool IsInterior { get; private set; }
public bool IsUnderground { get; private set; }
public bool IsArtificial { get; private set; }
public string Description { get; private set; }
public void SetName(string name)
{
Name = name;
UpdateIpfs();
}
public void SetDescription(string desc)
{
Description = desc;
UpdateIpfs();
}
public void SetInterior(bool interior)
{
IsInterior = interior;
UpdateIpfs();
}
public void SetUnderground(bool underground)
{
IsUnderground = underground;
UpdateIpfs();
}
public void SetArtificial(bool artificial)
{
IsArtificial = artificial;
UpdateIpfs();
}
public int X { get; set; }
public int Y { get; set; }
public int Z { get; set; }
public bool SetPosition(int x,int y, int z)
{
World world = (World)ParentObject;
var area = world.Areas.FirstOrDefault(e => e.X == x && e.Y == y && e.Z == z);
if(area != null)
{
return false;
}
this.X = x;
this.Y = y;
this.Z = z;
UpdateIpfs();
return true;
}
}
I can confirm that when I take the parameters out of the constructor for World/Area - that the deserialisation then preserves the reference link between world and area.
Suppose that I have some json that has 3 objects and values
{
"SomeProperty": "42",
"Foo": "bar",
"Name001": "ABC_1",
"Type001": "D",
"Confidence001": "100",
...
"Name00N": "ABC_N",
"Type00N": "D",
"Confidence00N": "50",
}
How would I use a converter/mapper to group those 3 objects into a single POCO?
public class DeserializedJsonClass
{
public long SomeProperty {get;set;}
public string Foo {get;set;}
public EvaluationStat[] Evaluations {get;set;}
}
public class EvaluationStat
{
public string Name {get;set;}
public char Type {get;set;}
public int Confidence {get;set;}
}
I've learned about the mapping that Newtonsoft has, and I use the converter to convert Y/N to bool and some xml into a XDocument but I can't figure out how to map those 3 objects into one.
One way to solve it:
public class DeserializedJsonClass
{
public long SomeProperty { get; set; }
public string Foo { get; set; }
public IEnumerable<EvaluationStat> Evaluations { get; set; }
}
public class EvaluationStat
{
public string Name { get; set; }
public char Type { get; set; }
public int Confidence { get; set; }
}
// ...
private static DeserializedJsonClass Deserialize(string json)
{
// using Newtonsoft.Json
dynamic deserialized = JsonConvert.DeserializeObject(json);
DeserializedJsonClass result = new DeserializedJsonClass();
result.SomeProperty = deserialized.SomeProperty;
result.Foo = deserialized.Foo;
result.Evaluations = FetchEvaluations(deserialized);
return result;
}
private static IEnumerable<EvaluationStat> FetchEvaluations(dynamic json)
{
ICollection<string> names = new List<string>();
ICollection<char> types = new List<char>();
ICollection<int> confidences = new List<int>();
foreach (var prop in json)
{
if (prop.Name.StartsWith("Name"))
names.Add((string)prop.Value.Value);
else if (prop.Name.StartsWith("Type"))
types.Add(Convert.ToChar((string)prop.Value.Value));
else if (prop.Name.StartsWith("Confidence"))
confidences.Add(Convert.ToInt32((string)prop.Value.Value));
}
return names.Zip(types, (n, t) => new { Name = n, Type = t })
.Zip(confidences, (l, c) => new { Name = l.Name, Type = l.Type, Confidence = c })
.Select(t => new EvaluationStat()
{
Name = t.Name,
Type = t.Type,
Confidence = t.Confidence
});
}
If you are able to change the source data, this JSON would be able to be deserialized into the Classes you have proposed.
[
{
"Name": "ABC_1",
"Type": "D",
"Confidence": "100"
},
...
{
"Name": "ABC_N",
"Type": "D",
"Confidence": "50"
}
]
Custom JsonConvert is another way to do it
public class EvaluationStatJsonConverter : JsonConverter
{
public override bool CanConvert( Type objectType ) { return objectType == typeof( DeserializedJsonClass ); }
public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer )
{
var jsonObject = JObject.Load( reader );
var properties = typeof( EvaluationStat ).GetProperties();
var deserializedJsonClass = new DeserializedJsonClass
{
Evaluations = new EvaluationStat[jsonObject.Count / properties.Length]
};
for( var i = 1; i <= jsonObject.Count / properties.Length; i++ )
{
deserializedJsonClass.Evaluations[ i - 1 ] = new EvaluationStat();
foreach( var field in properties )
{
field.SetValue( deserializedJsonClass.Evaluations[ i - 1 ],
jsonObject[ $"{field.Name}{i:000}" ].ToObject( field.PropertyType ) );
}
}
return deserializedJsonClass;
}
public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer )
{
throw new NotImplementedException();
}
}
then where ever you want use it you can do this
var jss = new JsonSerializerSettings();
jss.Converters.Add( new EvaluationStatJsonConverter() );
var result =
JsonConvert.DeserializeObject< DeserializedJsonClass >(
"your json content" ),
jss );
I do agree with the other comments it's quite a hack to get decent solution.
I have written following sample code to save a slightly complex object FamilyTreeFile to XML and restore it back to original form.
public class XmlSerializationTest
{
const string FileName = #"FamilyTree.xml";
public void Run()
{
var rootMember = new Member() { Name = "Johny", Parent = null };
var member1 = new Member() { Name = "Andy", Parent = rootMember };
var member2 = new Member() { Name = "Adam", Parent = rootMember };
var member3 = new Member() { Name = "Andrew", Parent = rootMember };
var member4 = new Member() { Name = "Davis", Parent = member2 };
var member5 = new Member() { Name = "Simon", Parent = member4 };
rootMember.FamilyTree = new GenericCollection();
rootMember.FamilyTree.Add(member1);
rootMember.FamilyTree.Add(member2);
rootMember.FamilyTree.Add(member3);
member2.FamilyTree = new GenericCollection();
member2.FamilyTree.Add(member4);
member4.FamilyTree = new GenericCollection();
member4.FamilyTree.Add(member5);
var familyTree = new GenericCollection() { rootMember };
IFamilyTreeFile file = new FamilyTreeFile()
{
FamilyTree = familyTree
};
Serialize(file);
file = Deserialize();
}
public void Serialize(IFamilyTreeFile obj)
{
var xmlSerializer = new XmlSerializer(typeof(FamilyTreeFile));
using (TextWriter writer = new StreamWriter(FileName))
{
xmlSerializer.Serialize(writer, obj);
}
}
public IFamilyTreeFile Deserialize()
{
XmlSerializer serializer = new XmlSerializer(typeof(FamilyTreeFile));
using (Stream stream = File.Open(FileName, FileMode.Open))
{
return (IFamilyTreeFile)serializer.Deserialize(stream);
}
}
}
public interface IMember
{
string Name { get; set; }
IMember Parent { get; set; }
GenericCollection FamilyTree { get; set; }
}
[Serializable]
public class Member : IMember
{
[XmlAttribute]
public string Name { get; set; }
[XmlIgnore]
public IMember Parent { get; set; }
public GenericCollection FamilyTree { get; set; }
public Member()
{
//FamilyTree = new GenericCollection();
}
}
[Serializable]
public class GenericCollection : List<IMember>, IXmlSerializable
{
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
public void ReadXml(XmlReader reader)
{
reader.MoveToContent();
if (reader.Name == "FamilyTree")
{
do
{
reader.Read();
if (reader.Name == "Member" && reader.IsStartElement())
{
Type type = System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
.Where(x => x.Name == reader.Name)
.FirstOrDefault();
if (type != null)
{
var xmlSerializer = new XmlSerializer(type);
var member = (IMember)xmlSerializer.Deserialize(reader);
this.Add(member);
}
}
if (reader.Name == "FamilyTree" && reader.NodeType == System.Xml.XmlNodeType.EndElement)
break;
}
while (!reader.EOF);
}
}
public void WriteXml(XmlWriter writer)
{
foreach (IMember rule in this)
{
var namespaces = new XmlSerializerNamespaces();
namespaces.Add(String.Empty, String.Empty);
XmlSerializer xmlSerializer = new XmlSerializer(rule.GetType());
xmlSerializer.Serialize(writer, rule, namespaces);
}
}
}
public interface IFamilyTreeFile
{
GenericCollection FamilyTree { get; set; }
}
public class FamilyTreeFile : IFamilyTreeFile
{
public GenericCollection FamilyTree { get; set; }
}
The code sample is generating the following XML file which is exactly as per my needs but i am unable to read it back using ReadXml method.
<?xml version="1.0" encoding="utf-8"?>
<FamilyTreeFile xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<FamilyTree>
<Member Name="Johny">
<FamilyTree>
<Member Name="Andy" />
<Member Name="Adam">
<FamilyTree>
<Member Name="Davis">
<FamilyTree>
<Member Name="Simon" />
</FamilyTree>
</Member>
</FamilyTree>
</Member>
<Member Name="Andrew" />
</FamilyTree>
</Member>
</FamilyTree>
</FamilyTreeFile>
I need help in how can i restore it back efficiently?
ADDED
Upon adding new collection Notes in IMember
public interface IMember
{
string Name { get; set; }
IMember Parent { get; set; }
GenericCollection FamilyTree { get; set; }
List<Note> Notes { get; set; }
}
[Serializable]
public class Note
{
[XmlAttribute]
public string Text { get; set; }
}
Implementing this property in Member class
[XmlArray("Notes")]
public List<Note> Notes { get; set; }
I am unable to deserialize Notes information at this line.
var member = (IMember)xmlSerializer.Deserialize(reader);
Isn't there any simple way to deserialize using XmlSerializer or any framework which handles everything itself?
Here is a working version of the GenericCollection.ReadXml method for you:
public void ReadXml(XmlReader reader)
{
// no need to advace upfront so MoveToContent was taken out (would
// mess with subsequent inner deserializations anyway)
// very important: there may be no members, so check IsEmptyElement
if (reader.Name == "FamilyTree" && !reader.IsEmptyElement)
{
do
{
if (reader.Name == "Member" && reader.IsStartElement())
{
Type type = System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
.Where(x => x.Name == reader.Name)
.FirstOrDefault();
if (type != null)
{
var xmlSerializer = new XmlSerializer(type);
var member = (IMember) xmlSerializer.Deserialize(reader);
this.Add(member);
}
continue; // to omit .Read because Deserialize did already
// advance us to next element
}
if (reader.Name == "FamilyTree" && reader.NodeType == XmlNodeType.EndElement)
break;
reader.Read();
} while (!reader.EOF);
}
}
What you will most propably have missed out on in your version, was the fact that every call to a method of the XmlReader like Read...() or Move...() does advance it's reading position. The inner deserialization of member objects does the same.
Keeping this in mind, it should become clear that you simply cannot always issue a Read() at the beginning of the loop, but only at the very end. This way you can skip it with the continue keyword, in case some other code in the loop body (like Deserialize() in our case) did already advance the XmlReader. Same applies to MoveToContent() at the beginning of your version of the method. What I initially did miss out on too, is the fact that the collection of members can be empty. In that case the deserialization of GenericCollection has to be omitted completely, as (again) not to mess up the reader.
While this does deserialize the object instances and adds them to their respective lists, the references (the Parent field of the Member class in this example) are not reconstructed. Here is where things get tricky: A reference is esentially a memory adress. Being that, there is no point in serializing it's value and deserializing it back again. Because the objects will most propably reside in another memory location now, the deserialized address would be entirely wrong.
There are basically two ways to solve this:
The serialized objects could be constructed in a manner that automatically creates those references, when the objects are constucted or glued together. This way there is simply no serialization and deserialization needed. The drawback is: This is only possible for references that can be obtained in this manner (is the case in the current example)
Every object that can be target of a reference cold be extended by an identifier field, quite similar to a primary key in a database. This identifier (for example a guid) is then to be serialized and deserialized. Every referece field (the Parent field of the Member class in this example) is to be serialized as identifier value of the object it references (could be done by adding a helper field ParentID, which is set automatically by the setter of the Parent filed). When everything is deserialized, these references have to be reconstructed by walking the entire tree of objects. On the plus side, this enables one to reconstruct arbitrary references. But one has to be aware of this adding some real complexity to the code.
First approach could be done by:
Changing this in your Run() function...
var rootMember = new Member() { Name = "Johny"};
var member1 = new Member() { Name = "Andy" };
var member2 = new Member() { Name = "Adam" };
var member3 = new Member() { Name = "Andrew" };
var member4 = new Member() { Name = "Davis" };
var member5 = new Member() { Name = "Simon" };
...change property FamilyTree of class Member to this...
public GenericCollection FamilyTree
{
get { return _FamilyTree; }
set
{
_FamilyTree = value;
_FamilyTree.Owner = this;
}
}
... and insert this into class GenericCollection
private IMember _Owner;
public IMember Owner
{
get { return _Owner; }
set
{
_Owner = value;
foreach (var member in this)
{
member.Parent = value;
}
}
}
public void Add(IMember item)
{
item.Parent = Owner;
base.Add(item);
}
The second approach is implemented in the following small console application:
class Program
{
public static string FileName = #"FamilyTree.xml";
static void Main(string[] args)
{
// make some members
var rootMember = new Member() { Name = "Johny" };
var member1 = new Member() { Name = "Andy" };
var member2 = new Member() { Name = "Adam" };
var member3 = new Member() { Name = "Andrew" };
var member4 = new Member() { Name = "Davis" };
var member5 = new Member() { Name = "Simon" };
// construct some arbitrary references between them
member1.Reference = member4;
member3.Reference = member1;
member5.Reference = member2;
// let member 3 have some notes
member3.Notes = new List<Note>();
member3.Notes.Add(new Note() { Text = "note1" });
member3.Notes.Add(new Note() { Text = "note2" });
// add all of the to the family tree
rootMember.FamilyTree.Add(member1);
rootMember.FamilyTree.Add(member2);
rootMember.FamilyTree.Add(member3);
member2.FamilyTree.Add(member4);
member4.FamilyTree.Add(member5);
var familyTree = new GenericCollection() { rootMember };
IFamilyTreeFile file = new FamilyTreeFile()
{
FamilyTree = familyTree
};
Console.WriteLine("--- input ---");
Serialize(file);
PrintTree(file.FamilyTree, 0);
Console.WriteLine();
Console.WriteLine("--- output ---");
file = Deserialize();
file.FamilyTree.RebuildReferences(file.FamilyTree); // this is where the refereces
// are put together again after deserializing the object tree.
PrintTree(file.FamilyTree, 0);
Console.ReadLine();
}
private static void PrintTree(GenericCollection c, int indent)
{
foreach (var member in c)
{
string line = member.Name.PadLeft(indent, ' ');
if (member.Reference != null)
{
line += " (Ref: " + member.Reference.Name + ")";
if (member.Notes != null && member.Notes.Count > 0)
{
line += " (Notes: ";
foreach (var note in member.Notes)
{
line += note.Text + ",";
}
line += ")";
}
}
Console.WriteLine(line);
PrintTree(member.FamilyTree, indent + 4);
}
}
public static void Serialize(IFamilyTreeFile obj)
{
var xmlSerializer = new XmlSerializer(typeof(FamilyTreeFile));
using (TextWriter writer = new StreamWriter(FileName))
{
xmlSerializer.Serialize(writer, obj);
}
}
public static IFamilyTreeFile Deserialize()
{
XmlSerializer serializer = new XmlSerializer(typeof(FamilyTreeFile));
using (Stream stream = File.Open(FileName, FileMode.Open))
{
return (IFamilyTreeFile)serializer.Deserialize(stream);
}
}
}
public interface IMember
{
Guid ID { get; set; }
string Name { get; set; }
IMember Reference { get; set; }
Guid ReferenceID { get; set; }
GenericCollection FamilyTree { get; set; }
List<Note> Notes { get; set; }
void RebuildReferences(GenericCollection in_Root);
}
[Serializable]
public class Member : IMember
{
private GenericCollection _FamilyTree;
private IMember _Reference;
[XmlAttribute]
public Guid ID { get; set; }
[XmlAttribute]
public string Name { get; set; }
[XmlAttribute]
public Guid ReferenceID { get; set; }
[XmlIgnore]
public IMember Reference
{
get { return _Reference; }
set
{
ReferenceID = value.ID;
_Reference = value;
}
}
[XmlArray("Notes")]
public List<Note> Notes { get; set; }
public GenericCollection FamilyTree
{
get { return _FamilyTree; }
set
{
_FamilyTree = value;
_FamilyTree.Owner = this;
}
}
public Member()
{
ID = Guid.NewGuid();
FamilyTree = new GenericCollection();
}
public void RebuildReferences(GenericCollection in_Root)
{
if (!ReferenceID.Equals(Guid.Empty))
Reference = in_Root.FindMember(ReferenceID);
FamilyTree.RebuildReferences(in_Root);
}
}
[Serializable]
public class Note
{
[XmlAttribute]
public string Text { get; set; }
}
[Serializable]
public class GenericCollection : List<IMember>, IXmlSerializable
{
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
private IMember _Owner;
public IMember Owner
{
get { return _Owner; }
set
{
_Owner = value;
}
}
public void Add(IMember item)
{
base.Add(item);
}
public void ReadXml(XmlReader reader)
{
// no need to advace upfront so MoveToContent was taken out (would
// mess with subsequent inner deserializations anyway)
// very important: there may be no members, so check IsEmptyElement
if (reader.Name == "FamilyTree" && !reader.IsEmptyElement)
{
do
{
if (reader.Name == "Member" && reader.IsStartElement())
{
Type type = System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
.Where(x => x.Name == reader.Name)
.FirstOrDefault();
if (type != null)
{
var xmlSerializer = new XmlSerializer(type);
var member = (IMember)xmlSerializer.Deserialize(reader);
this.Add(member);
}
continue; // to omit .Read because Deserialize did already
// advance us to next element
}
if (reader.Name == "FamilyTree" && reader.NodeType == XmlNodeType.EndElement)
break;
reader.Read();
} while (!reader.EOF);
}
}
public void WriteXml(XmlWriter writer)
{
foreach (IMember rule in this)
{
var namespaces = new XmlSerializerNamespaces();
namespaces.Add(String.Empty, String.Empty);
XmlSerializer xmlSerializer = new XmlSerializer(rule.GetType());
xmlSerializer.Serialize(writer, rule, namespaces);
}
}
public void RebuildReferences(GenericCollection in_Root)
{
foreach (IMember meber in this)
{
meber.RebuildReferences(in_Root);
}
}
public IMember FindMember(Guid in_ID)
{
IMember FoundMember = null;
foreach (IMember member in this)
{
if (member.ID.Equals(in_ID))
return member;
FoundMember = member.FamilyTree.FindMember(in_ID);
if (FoundMember != null)
return FoundMember;
}
return null;
}
}
public interface IFamilyTreeFile
{
GenericCollection FamilyTree { get; set; }
}
public class FamilyTreeFile : IFamilyTreeFile
{
public GenericCollection FamilyTree { get; set; }
}
A proof of concept for your addition to the original question is disclosed within this second example.
I'm having a hard time overriding the WriteJson method of a custom JsonConverter in order to slightly alter the way serialization is performed.
I need to call a REST service that accepts a certain input that has a generic portion. I can reproduce the problem I'm having with the following payload format :
public sealed class JsonField
{
public string Key { get; set; }
public object Value { get; set; }
public string OtherParam { get; set; }
}
public sealed class JsonPayload
{
public string Item1 { get; set; }
public string Item2 { get; set; }
public List<JsonField> Fields { get; set; }
}
The REST API I'm calling needs to have Fields be an object containing as many fields with names corresponding to the Key properties specified in the original collection. Like so:
{
"Item1" : "Value1",
"Item2" : "Value2",
...
"Fields":
{
"Key1": {"Value":"Value1", "OtherParam":"other1"}
"Key2": {"Value":42, "OtherParam":"other2"}
}
}
However, using the default options, the payload is serialized like this:
{
"Item1" : "Value1",
"Item2" : "Value2",
...
"Fields":[
{ "Key":"Key1", "Value":"Value1", "OtherParam":"other1" }
{ "Key":"Key2", "Value":42, "OtherParam":"other2" }
]
}
You notice that there is a collection of objects where I would like a single object.
Also, I would like the Key names to be the names of individual properties in the Fields.
I have a hard time figuring out how to use the JToken, JProperty, JValue object in my custom converter. I have, in fact, never attempted such a thing so I find it hard to wrap my head around those concepts.
I have tried creating two custom converters, one at the scope of the class, in order to prevent the collection from being generated, and the second one at the scope of the collection, but so far without success.
Here is what I have tried:
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var token = JToken.FromObject(value);
var key = token["Key"];
if (key == null)
throw new Exception("missing key");
var propertyName = key.ToString();
var json = new StringBuilder();
json.Append("{");
foreach (var child in token.Children())
{
var property = child as JProperty;
if (property == null || property.Name == "Key")
continue;
var propName = property.Name;
var propValue = JsonConvert.SerializeObject(property.Value);
json.AppendFormat("\"{0}\": {1},", propName, propValue);
}
if (json.Length > 1)
json.Remove(json.Length - 1, 1);
json.Append("}");
var newToken = JToken.Parse(json.ToString());
var serializedObject = JsonConvert.SerializeObject(newToken);
writer.WriteStartObject();
writer.WritePropertyName(propertyName);
writer.WriteToken(newToken.CreateReader());
writer.WriteEndObject();
}
Is there a way to perform what I want to achieve?
Maybe I'm approaching the problem the wrong way, so please, if you have easier alternatives, then by all means do not hesitate to share what you have in mind.
You're on the right track. You only need a single converter here -- for the list of JsonField objects --and the code is actually very straightforward. This is all you need:
class JsonFieldListConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(List<JsonField>));
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JObject containerObj = new JObject();
foreach (JsonField field in (List<JsonField>)value)
{
JObject itemObj = new JObject();
itemObj.Add("Value", JToken.FromObject(field.Value));
itemObj.Add("OtherParam", new JValue(field.OtherParam));
containerObj.Add(field.Key, itemObj);
}
containerObj.WriteTo(writer);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Here's a demo showing the converter in action:
class Program
{
static void Main(string[] args)
{
JsonPayload payload = new JsonPayload
{
Item1 = "Value1",
Item2 = "Value2",
Fields = new List<JsonField>
{
new JsonField { Key = "Key1", Value = "Value1", OtherParam = "other1" },
new JsonField { Key = "Key2", Value = 42, OtherParam = "other2" },
}
};
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new JsonFieldListConverter());
settings.Formatting = Formatting.Indented;
string json = JsonConvert.SerializeObject(payload, settings);
Console.WriteLine(json);
}
}
Output:
{
"Item1": "Value1",
"Item2": "Value2",
"Fields": {
"Key1": {
"Value": "Value1",
"OtherParam": "other1"
},
"Key2": {
"Value": 42,
"OtherParam": "other2"
}
}
}
Short answer:
You can do this simply by changing the List<T> to a Dictionary<T>
Answer with an example:
You can do this simply by changing the List<T> to a Dictionary<T> this will give you what appears to be a single object with the property names as the keys of the dictionary. If the class contains the key this will also be serialized, although you could [JsonIgnore] it but that would mean it wouldn't be populated during deserialization.
void Main()
{
var pl = new JsonPayload()
{Item1 = "Value1",Item2 = "Value2"};
var field1 = new JsonField() { Key = "Key1", Value = "Value1", OtherParam = "other1" };
var field2 = new JsonField() { Key = "Key2", Value = "Value2", OtherParam = "other2" };
pl.Fields = new Dictionary<string, JsonField>() { { field1.Key , field1}, { field2.Key, field2 }};
string json = JsonConvert.SerializeObject(pl);
JsonPayload pl2 = JsonConvert.DeserializeObject<JsonPayload>(json);
string output = JsonConvert.SerializeObject(pl2);
output.Dump();
}
public sealed class JsonField
{
public string Key { get; set; }
public object Value { get; set; }
public string OtherParam { get; set; }
}
public sealed class JsonPayload
{
public string Item1 { get; set; }
public string Item2 { get; set; }
public Dictionary<string, JsonField> Fields { get; set; }
}