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.
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 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);
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.
Could anyone provide their approach for deserializing the following JSON
{
"i": 5
"b0": "ABC",
"b1": "DEF",
"b2": "GHI",
"s0": "SABC",
"s1": "SDEF",
"s2": "SGHI",
}
into a class in C# to provide the same structure as this
class Example {
public int Index {get;set;}
public string[] B {get;set;}
public string[] S {get;set;}
}
var b = new [] {"ABC", "DEF", "GHI"}
var s = new [] {"SABC", "SDEF", "SGHI"}
I generally use ServiceStack.Text, but Json.Net approach or even a BsonDocument from the MongoDb provider is fine.
Could use a JToken and use .Values() then .ToArray():
var json = "{\r\n \"b0\": \"ABC\",\r\n \"b1\": \"DEF\",\r\n \"b2\": \"GHI\",\r\n}";
var token = JToken.Parse(json);
var b = token.Values().ToArray();
This one solution might be useful too:
public class Example
{
public int Index { get; set; }
public string[] B { get; set; }
public string[] S { get; set; }
}
var strData = #"{'i': 5, 'b0': 'ABC','b1': 'DEF', 'b2': 'GHI', 's0': 'SABC', 's1': 'SDEF', 's2': 'SGHI',}";
var data = JsonConvert.DeserializeObject<JToken>(strData).Values().ToList();
Example result = new Example();
result.Index = data.Values().FirstOrDefault(x => x.Path == "i").Value<int>();
result.B = data.Values().Where(x => x.Path.StartsWith("b")).Select(x => x.Value<string>()).ToArray();
result.S = data.Values().Where(x => x.Path.StartsWith("s")).Select(x => x.Value<string>()).ToArray();
there is solution for dynamic name of arrays, but string array is not the best type in this case:
public class Example
{
public int Index { get; set; }
public Dictionary<string, string[]> Data { set; get; }
}
var s = #"{'i': 5, 'b0': 'ABC','b1': 'DEF', 'b2': 'GHI', 's0': 'SABC', 's1': 'SDEF', 's2': 'SGHI',}";
var data = JsonConvert.DeserializeObject<JToken>(s).Values().ToList();
Example result = new Example();
result.Index = data.Values().FirstOrDefault(x => x.Path == "i").Value<int>();
result.Data = new Dictionary<string, string[]>();
var stringData = data.Values().Where(x => x.Path != "i").ToList();
stringData.ForEach(x =>
{
var key = x.Path[0].ToString();
if (!result.Data.ContainsKey(key))
{
result.Data.Add(key, new string[0]);
}
var currentValue = result.Data[key].ToList();
currentValue.Add(x.Value<string>());
result.Data[key] = currentValue.ToArray();
});
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; }
}