I have a Class like this that I want to serialize and deserialize
using Newtonsoft.Json;
TestClass testClass = new TestClass();
testClass.Foo = "Foo";
testClass.Bar[0] = 3;
// to JSON
string json = JsonConvert.SerializeObject(testClass);
// and back
TestClass result = JsonConvert.DeserializeObject<TestClass>(json)!;
/// <summary>
/// External class that I cannot change
/// </summary>
public class TestClass
{
public string Foo { get; set; }
public int[] Bar { get; } = new int[3];
}
Serializing works well, but at deserializing "Bar" is not writeable, so skipped and has default values.
Is there a way to tell Json.net to deserialize element by element of the (maybe reaonly arrays?) array and set it as value for the corresponding array index?
you can try this code
var jsonObj=JObject.Parse(json);
TestClass result=jsonObj.ToObject<TestClass>();
for (var i=0; i < ((JArray) jsonObj["Bar"]).Count; i++)
result.Bar[i]= (int) jsonObj["Bar"][i];
At least for this simple example I have a solution. Has to be tested on more complex structures.
public class TestConverter : JsonConverter
{
private bool skipOnce = false;
public override bool CanConvert(Type objectType)
{
if (skipOnce)
{
skipOnce = false;
return false;
}
return objectType.GetProperties().Any(prop => !prop.CanWrite && prop.PropertyType.IsArray);
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
skipOnce = true; // our manual serializer.Deserialize will cause endless recursion otherwise
JObject jo = JObject.Load(reader);
reader = jo.CreateReader(); // reset reader
var obj = serializer.Deserialize(reader, objectType);
var readOnlyArrayProperties = objectType.GetProperties().Where(prop => !prop.CanWrite && prop.PropertyType.IsArray);
foreach (var readOnlyProperty in readOnlyArrayProperties)
{
var readOnlyArray = (Array)readOnlyProperty.GetValue(obj)!;
Type elementType = readOnlyArray.GetType().GetElementType()!;
var jsonArrayProp = (JArray)jo[readOnlyProperty.Name]!;
for (int i = 0; i < readOnlyArray.Length; i++)
{
readOnlyArray.SetValue(jsonArrayProp[i].ToObject(elementType, serializer), i);
}
}
return obj;
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Basic idea: If we have readonly array properties in the current object we do serializer.Deserialize manually and additionaly we set the value for every array index.
[EDIT] After test with complex structure: Seems to be more complicated. Sometimes existingValueis set. In these cases json.net seems to ignore our returned obj. populating into existingValue seems to work at first glance.
Related
The Problem
I am creating an anonymous object in a method and assigning it properties from a class. Some of the properties can sometimes be empty (strings) and I eventually need to serialize the anonymous object to a JSON string.
When it gets serialized as JSON, I want to get rid of nulls or empty strings. I actually want to do this on a property by property basis (not generically ignoring all nulls) but it doesn't seem to be working at all!
The Code
public class Refrigerator {
public string Brand { get; set; }
public bool HasFood { get; set; }
public void Serialize() {
//ANONYMOUS OBJECT HERE
var fridge = new {
Brand = this.Brand; //THIS VALUE IS SOMETIMES AN EMPTY STRING!!
HasFood = this.HasFood;
}
var value = Newtonsoft.Json.JsonConvert.SerializeObject(fridge);
//value contains Brand as an empty string
}
}
What I've Tried
var fridge = new {
Brand = this.Brand;
HasFood = this.HasFood;
//JsonSerializer should check this function I thought?
ShouldSerializeBrand = new Func<bool>() {
if(String.IsNullOrEmpty(this.Brand){
return false;
}
return true;
}
}
//This should also work, but doesn't.......
var value = Newtonsoft.Json.JsonConvert.SerializeObject(fridge, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore
});
//JSON STILL contains Brand as an empty string!!! Its still there!
Any ideas why its not working?
One of the way for this could be that, you can use ExpandoObject for this like :
dynamic fridge = new ExpandoObject();
if(!String.IsNullOrEmpty(this.Brand))
{
fridge.Brand = this.Brand;
}
fridge.HasFood = this.HasFood;
This way the anonymous object will not have Brand property created unless it has a value. Now if the Brand is null of empty string, the property won't be created on the anonymous object we have created.
See this DEMO FIDDLE as example.
An empty string i.e. the literal "" is neither a null value nor the default for a string type. As such, both options from your JsonSerializerSettings will fail to do what you want. You can solve the problem by doing what you already tried, or by creating a custom JSONConverter to add in your specific serialization logic.
Sample code based off the Newtonsoft samples:
public class FridgeJsonConverter : JsonConverter
{
private readonly Type[] _types;
public FridgeJsonConverter(params Type[] types)
{
_types = types;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JToken t = JToken.FromObject(value);
if (t.Type != JTokenType.Object)
{
t.WriteTo(writer);
}
else
{
JObject o = (JObject)t;
var brandProp = o.Properties().First(x => x.Name == "Brand");
if(brandProp.Value.ToString() == string.Empty)
o.Remove(brandProp.Name);
o.WriteTo(writer);
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException("Unnecessary because CanRead is false. The type will skip the converter.");
}
public override bool CanRead
{
get { return false; }
}
public override bool CanConvert(Type objectType)
{
return _types.Any(t => t == objectType);
}
}
Then, in your calling code, do this:
string json = JsonConvert.SerializeObject(fridge, Formatting.Indented, new FridgeJsonConverter(typeof(Fridge)));
I am just wonder if I can mark certain property of class instance via any attribute and during serialization serialize just those marked properties (and of-course by deserializing affect also only marked properties via attribute vice-versa in instance of the class - the rest of properties should remain same...).
I know how to identify those properties by reflection, but I do not want to make another Json serialization by myself.
[MyFirstAttribute]
public string A { get; set; } = "hi";
[MyFirstAttribute]
public int B { get; set; } = 13;
[MySecondAttribute]
public string C { get; set; } = "something";
as it documented here in this link you can create a custom CustomJsonConverter by inheriting from JsonConverter class.
And then use it like:
Employee employee = new Employee
{
FirstName = "James",
LastName = "Newton-King",
Roles = new List<string>
{
"Admin"
}
};
string json = JsonConvert.SerializeObject(employee, Formatting.Indented, new KeysJsonConverter(typeof(Employee)));
Console.WriteLine(json);
Based on #ArgeKumandan advice:
public class KeysJsonConverter : JsonConverter
{
private readonly Type[] _types;
public KeysJsonConverter(params Type[] types) => _types = types;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JToken t = JToken.FromObject(value);
if (t.Type != JTokenType.Object) t.WriteTo(writer);
else
{
JObject jo = new JObject();
foreach (PropertyInfo prop in value.GetType().GetProperties())
{
if (!prop.CanRead) continue;
object propVal = prop.GetValue(value, null);
if (propVal is null || !Attribute.IsDefined(prop, _types[0])) continue;
jo.Add(prop.Name, JToken.FromObject(propVal, serializer));
}
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException("Unnecessary because CanRead is false. The type will skip the converter.");
}
public override bool CanRead { get => false; }
public override bool CanConvert(Type objectType) => _types.Any(t => t == _types[0]);
}
and then usage:
// serialization
var json = JsonConvert.SerializeObject(objectInstance1, Formatting.Indented, new KeysJsonConverter(typeof(MyFirstAttribute)));
// deserialization to an existing instance that updates just the properties contained in JSON
JsonConvert.PopulateObject(jsonCommon, objectInstance2);
I am developing a game which has a class called UserInfo.
Inside the UserInfo class there is a public List<Lineup> lineups variable.
And Lineup contains public Dictionary<Vector2Int, Collection> cardLocations.
My problem is, JsonConvert.SerializeObject does correctly produce the result I want, but when I do JsonConvert.DeserializeObject<UserInfo>, Vector2Int remains to be a string like "(x, y)".
How should I fix it?
I wrote a JsonConverter to convert it, and used
JsonConvert.SerializeObject(userInfo, Formatting.Indented, new Vec2Converter())
for serialization and
JsonConvert.DeserializeObject<UserInfo>(json, new Vec2Converter())
to deserialization, but it doesn't work.
Here is my JsonConverter script:
using Newtonsoft.Json;
using UnityEngine;
using System;
public class Vec2Converter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var vector2Int = serializer.Deserialize<Vector2Int>(reader);
Debug.Log(vector2Int);
return vector2Int;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Vector2Int);
}
}
Another weird thing is that, when I tried to change Vector2Int to other data structure like Lineup or Dictionary<Vector2Int, Collection>, Unity exited when I clicked play. After I changed it back, it became ok. The ok means it's not exiting but still giving me the error message.
Forgot to put the error message: ArgumentException: Could not cast or convert from System.String to UnityEngine.Vector2Int
Here are the classes.
public class UserInfo {
public string username;
public int playerID;
public List<Collection> collection = new List<Collection>();
public List<Lineup> lineups = new List<Lineup>(); // Here is the problem
public Dictionary<string, int> contracts = new Dictionary<string, int>();
public int coins = 0;
public int rank = 0;
public int lastLineupSelected = -1;
public int winsToday = 0;
public Stats total = new Stats();
public Dictionary<string, Stats> boardResults = new Dictionary<string, Stats>();
public List<Mission> missions;
public string preferredBoard = "Standard Board";
public string lastModeSelected = "";
public int gameID;
public bool missionSwitched = false;
}
public class Lineup
{
public Dictionary<Vector2Int, Collection> cardLocations; // Here is the problem
public List<Tactic> tactics;
public string boardName;
public string lineupName;
public string general;
public bool complete;
}
public class Collection
{
public string name = "";
public string type = "";
public int count = 1;
public int health = 0;
public int oreCost = 0;
}
Your custom serializer that derives from JsonConverter needs more work and there are few errors. First, note that you are trying to serialize/de-serialize Dictionary of Vector2Int not just a Vector2Int variable.
It's this:
public Dictionary<Vector2Int, Collection> cardLocations.
not
public Vector2Int cardLocations;
Because of the above statement, your override bool CanConvert(Type objectType) function should be checking for typeof(Dictionary<Vector2Int, Collection>) not typeof(Vector2Int). Also, you also need to add checks to determine when to run the custom de-serializer.
Serializing:
1.In the WriteJson function that serializes the json, you only need to customize the Dictionary<Vector2Int, Collection> part which is throwing the exception so make sure that the custom code runs only when the type is Dictionary<Vector2Int, Collection>. You can do this by putting the code inside if (value is Dictionary<Vector2Int, Collection>).
2.Obtain the Dictionary to serialize from the second argument. Call writer.WriteStartArray() so that you'll write every value as an array. Now, loop over the Dictionary, write the key with writer.WriteValue(key) then write the value with serializer.Serialize(writer, entry.Value);.
3. After/outside the loop call writer.WriteStartArray(); then return from that function.
If the if (value is Dictionary<Vector2Int, Collection>) from #1 is false then simply call writer.WriteStartObject() followed by writer.WriteEndObject().
De-Serializing:
4.Determine when to run the custom de-serializer
We used if (value is Dictionary<Vector2Int, Collection>) in #1 when serializing to determine if the custom serializer code should run but for de-serializing, we use if (reader.TokenType == JsonToken.StartArray) to determine if we have reached the array part where we did our custom serialization in the ReadJson function.
5.Use JArray.Load(reader); to obtain the array data we serialized. In the returned array, the first element in it is the key in the Dictionary.The second element is the value in the Dictionary. The third element is the second key in the Dictionary. The fourth element is the second value in the Dictionary ans so on.
6.Separate the keys and values in the JArray.
To simplify this, loop over the JArray increment by 2 instead of 1. By incrementing by 2, the key can be easily be retrieved with JArray[loop + 0] and the value can be retrieved in the-same loop with JArray[loop + 1].
for (int i = 0; i < jArray.Count; i += 2)
{
//Key
string key = jArray[i + 0].ToString();
//Value
string value = jArray[i + 1].ToString();
}
7.Get the key in Vector2Int format.
Simply de-serialize the key from #6 into Vector2Int with JsonConvert.DeserializeObject<Vector2Int>(key).
8.Get the value in Collection format.
Simply de-serialize the value from #6 into Collection with JsonConvert.DeserializeObject<Collection>(value).
9.Reconstruct the data
Create new instance of Dictionary<Vector2Int, Collection> then add both the key from #7 and value from #8 to it and then return it from the ReadJson function.
If the if (reader.TokenType == JsonToken.StartArray) from #4 is false, simply create and return new instance of Dictionary<Vector2Int, Collection> from the ReadJson function without adding key or value to it.
class Vec2DictionaryConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Dictionary<Vector2Int, Collection>).IsAssignableFrom(objectType);
}
//Deserialize json to an Object
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
//Debug.Log("De-serializing!");
if (reader.TokenType == JsonToken.StartArray)
{
// Load JArray from stream
JArray jArray = JArray.Load(reader);
//Where to re-create the json data into
Dictionary<Vector2Int, Collection> dict = new Dictionary<Vector2Int, Collection>();
if (jArray == null || jArray.Count < 2)
{
return dict;
}
//Do the loop faster with +=2
for (int i = 0; i < jArray.Count; i += 2)
{
//first item = key
string firstData = jArray[i + 0].ToString();
//second item = value
string secondData = jArray[i + 1].ToString();
//Create Vector2Int key data
Vector2Int vect = JsonConvert.DeserializeObject<Vector2Int>(firstData);
//Create Collection value data
Collection values = JsonConvert.DeserializeObject<Collection>(secondData);
//Add both Key and Value to the Dictionary if key doesnt exit yet
if (!dict.ContainsKey(vect))
dict.Add(vect, values);
}
//Return the Dictionary result
return dict;
}
return new Dictionary<Vector2Int, Collection>();
}
//SerializeObject to Json
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
//Debug.Log("Serializing!");
if (value is Dictionary<Vector2Int, Collection>)
{
//Get the Data to serialize
Dictionary<Vector2Int, Collection> dict = (Dictionary<Vector2Int, Collection>)value;
//Loop over the Dictionary array and write each one
writer.WriteStartArray();
foreach (KeyValuePair<Vector2Int, Collection> entry in dict)
{
//Write Key (Vector)
serializer.Serialize(writer, entry.Key);
//Write Value (Collection)
serializer.Serialize(writer, entry.Value);
}
writer.WriteEndArray();
return;
}
writer.WriteStartObject();
writer.WriteEndObject();
}
}
Usage:
Serialize
string json = JsonConvert.SerializeObject(userInfo, new Vec2DictionaryConverter());
De-serialize
UserInfo obj = JsonConvert.DeserializeObject<UserInfo>(json, new Vec2DictionaryConverter());
Manually parse the string and create the desired type.
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
var vector2IntString = reader.Value.ToString();//expecting "(x, y)"
Debug.Log(vector2IntString);
var parts = vector2IntString.Split(new char[]{ '(', ')', ',', ' '}, StringSplitOptions.RemoveEmptyEntries);
var x = int.Parse(parts[0]);
var y = int.Parse(parts[1]);
var vector2Int = new Vector2Int(x, y);
return vector2Int;
}
The above takes the "(x, y)" string and extracts the x and y values. It converts them to integers and uses them to initialize an instance of the desired type (Vector2Int)
Some further reading about this particular scenario revealed
If you only need a type conversion for JSON, you can also use JSON.NET's own custom type converters. Although this doesn't work in the case of dictionary keys.
Lastly, if you have a complex type as your dictionary key, you may want to consider just serializing it as a collection and then deserializing it using a JSON.NET custom converter.
For somebody like me that will run into the same issue.
#Programmer's answer is correct, but may be outdated (I don't know how Json.NET looked like in 2018) and it's too much code (at least for me).
Here's my solution:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
...
[JsonConverter(typeof(Vec2DictionaryConverter))]
public Dictionary<Vector2Int, Collection> cardLocations;
...
public class DictionaryConverter : JsonConverter<Dictionary<Vector2Int, Collection>> {
private static readonly string[] SEPARATORS = new[] { "(", ", ", ")" };
public override Dictionary<Vector2Int, Value> ReadJson(JsonReader reader, Type objectType,
Dictionary<Vector2Int, Collection> existingValue, bool hasExistingValue, JsonSerializer serializer) {
if (JsonToken.Null == reader.TokenType) {
return null;
}
var dictionary = new Dictionary<Vector2Int, Collection>();
foreach (var pair in JObject.Load(reader)) {
var vector = pair.Key.Split(SEPARATORS, StringSplitOptions.RemoveEmptyEntries)
.Select(it => Convert.ToInt32(it));
var key = new Vector2Int(vector.First(), vector.Last());
var value = pair.Value.ToObject<Collection>(serializer);
dictionary.Add(key, value);
}
return dictionary;
}
public override void WriteJson(JsonWriter writer,
Dictionary<Vector2Int, Collection> value, JsonSerializer serializer) {
serializer.Serialize(writer, value);
}
}
JsonConverter Attribute is used instead of passing Converter to SerializeObject/DeserializeObject all the time
Generic JsonConverter<Dictionary<Vector2Int, Collection>> is used, so no need to implement CanConvert anymore
Only ReadJson is implemented as OP didn't have problems with Serialization
This works fine and dandy, although we rely on Vector2Int.ToString implementation, but as I found out Unity's JsonUtility serialization is faster at least in my tests.
My application is consuming an API, and I'm trying to deserialize data of images coming back. The data is formatted like:
{
"images":{
"totalCount":4,
"0":{
"url":"file1.jpg"
},
"1":{
"url":"file2.jpg"
},
"2":{
"url":"file3.jpg"
},
"3":{
"url":"file4.jpg"
}
}
}
I have these model classes:
public class MyViewModel
{
[JsonProperty("images")]
public ImagesViewModel Images { get; set; }
}
public class ImagesViewModel
{
[JsonProperty("totalCount")]
public int TotalCount { get; set; }
public Dictionary<string, ImageViewModel> ListImages { get; set; }
}
public class ImageViewModel
{
[JsonProperty("url")]
public string Url { get; set; }
}
The collection of images isn't really a collection, for some reason it's just a new property for each image. I'm trying to deserialize my object like:
... // create HttpClient object, add headers and such
System.Net.Http.HttpResponseMessage response = await
client.GetAsync(endpointUrl);
var jsonString = response.Content.ReadAsStringAsync();
MyViewModel model =
JsonConvert.DeserializeObject<MyViewModel>(jsonString.Result);
I get back the totalCount property just fine, but the collection of images is coming back null.
Is there a way for me to change my view models so that I can deserialize the json correctly?
Given the formatting of the JSON you will have to go the long route and try to deserialize it using JObjects
//using Newtonsoft.Json.Linq
var jObject = JObject.Parse(jsonString);
var images = jObject.Property("images").Value<JObject>(); ;
var viewModel = new MyViewModel {
Images = new ImagesViewModel {
TotalCount = images.Property("totalCount").Value<int>(),
ListImages = images.Properties().Skip(1).ToDictionary(p => p.Name, p => p.Value<ImageViewModel>())
}
};
Going a step further and using a JsonConverter for converting the payload itself actually works as well given that we know now how to convert it.
public class MyViewModelConverter : JsonConverter {
public override bool CanConvert(Type objectType) {
return objectType == typeof(MyViewModel);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
var jObject = JObject.Load(reader);//<-- Note the use of Load() instead of Parse()
var images = jObject.Property("images").Value<JObject>(); ;
var model = new MyViewModel {
Images = new ImagesViewModel {
TotalCount = images.Property("totalCount").Value<int>(),
ListImages = images.Properties().Skip(1).ToDictionary(p => p.Name, p => p.Value<ImageViewModel>())
}
};
return model;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
throw new NotImplementedException();
}
}
and decorating the class itself
[JsonConverter(typeof(MyViewModelConverter))]
public class MyViewModel {
[JsonProperty("images")]
public ImagesViewModel Images { get; set; }
}
Deserialization is now as you intended to do before
var jsonString = await response.Content.ReadAsStringAsync();
MyViewModel model = JsonConvert.DeserializeObject<MyViewModel>(jsonString);
.NET Abhors dynamic types. They fly in the face of solid type checking at compile time. That being said, there is support for it:
As the example data is basically just a array of images, any collection could deal with this input.
If you can not even define the types umanbigiously (you might have a array of images and one of strings), the only way is ExpandoObject. It is designed specifically to deal with such cases. It is basically a List[string, object] with some Syntax Sugar, but it also does includes functions like Property Change Notifications.
Sounds like a job for a custom converter!
A custom converter will let you supply your own logic for deserializing specific types. Newtonsoft uses the target class to figure out with type if expects to find in the json and call the appropriate converter.
class ImagesViewModelConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(ImagesViewModel);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
assertToken(JsonToken.StartObject);
var obj = new ImagesViewModel()
{
ListImages = new Dictionary<string, ImageViewModel>()
};
while (reader.Read() && reader.TokenType != JsonToken.EndObject)
{
assertToken(JsonToken.PropertyName);
var propName = (string)reader.Value;
if (propName.Equals(nameof(ImagesViewModel.TotalCount), StringComparison.InvariantCultureIgnoreCase))
{
reader.Read();
assertToken(JsonToken.Integer);
obj.TotalCount = (int)((Int64)reader.Value);
continue;
}
reader.Read();
var image = serializer.Deserialize<ImageViewModel>(reader); // you can still use normal json deseralization inside a converter
obj.ListImages.Add(propName, image);
}
return obj;
void assertToken(JsonToken token)
{
if (reader.TokenType != token)
throw new Exception(); // might wanna add detailed errors
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException(); // implement if needed
}
}
And then:
var settings = new JsonSerializerSettings()
{
Converters = new[] { new ImagesViewModelConverter() }
};
var obj = JsonConvert.DeserializeObject<MyViewModel>(jsonString, settings);
});
You can even change classes to be easier to handle, given that they no longer need to match the json exactly. You can for example replace the dict with an array and have the converter fill it in order.
I have this kind of class:
class Thing
{
// ...
public IDictionary<string, dynamic> Parameters { get; set; }
}
//...
void Test()
{
Thing thing = new Thing();
thing.Paramaters["counting"] = new List<int>{1,2,3};
thing.Parameters["name"] = "Numbers";
thing.Parameters["size"] = 3;
string result = JsonConvert.SerializeObject(thing);
}
And I'd like to result to contain this:
{
"Parameters" :
{
"counting" : "[1,2,3]",
"name" : "Numbers",
"size" : 3
}
}
I have taken a look at IContractResolver and believe I should special case strings on deserialization to check if there's JSON in them, and special case all class objects to convert them to a string. I just have no idea where to begin doing that.
In the end the problem is this: the data structure I'm plugging this JSON into does not work with nested JSON. It only knows about the basic data, i.e. string and numbers, at this "sublevel". I know, terrible, get rid of this evil data structure. Well, I can't. So I need to be creative and I thought this might be a way out. If anyone can think of a better way, I'd much appreciate it!
EDIT The answers below special case the List<int> example I put in Test, but it's still a Dictionary<string, dynamic> which can contain everything. That's what I mean with nested JSON: any JSON, not just an array.
I understand the problem is not so much about writing a List to a string, but more about a way to create JSON with only two levels of depth - having everything beyond that a string.
I don't think an IContractResolver would work for this, you should implement a JsonConverter instead. The basic idea would be that it iterates over your object's children, then over their children, checking their type. If they're an array or an object - it would replace them with a serialized string.
Implementation:
class TwoDepthJsonConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var jo = JObject.FromObject(value);
foreach (var property in jo)
{
foreach (var parameter in property.Value)
{
var paramVal = parameter.First;
if (paramVal.Type == JTokenType.Array || paramVal.Type == JTokenType.Object)
{
paramVal.Replace(JsonConvert.SerializeObject(paramVal.ToObject<object>()));
}
}
}
jo.WriteTo(writer);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return JToken.ReadFrom(reader).ToObject(objectType);
}
public override bool CanConvert(Type objectType)
{
return true;
}
}
Usage:
Thing thing = new Thing();
thing.Parameters["counting"] = new List<int> { 1, 2, 3 };
thing.Parameters["name"] = "Numbers";
thing.Parameters["size"] = 3;
string result = JsonConvert.SerializeObject(thing, Formatting.Indented, new TwoDepthJsonConverter());
// Results:
// {
// "Parameters": {
// "counting": "[1,2,3]",
// "name": "Numbers",
// "size": 3
// }
// }
Of course, performance could be improved - for example writing to the writer manually instead of parsing to a JObject and then manipulating it. However this should be a good starting point.
You can create a JsonConverter and instead of using List<T> you would use a custom class that inherits from List<T>..
The reason for it needs to be a custom List is that if you register a JsonConverter for List<T> you would not be able to get normal array serialization.
Following http://blog.maskalik.com/asp-net/json-net-implement-custom-serialization/ I was able to make this.
public class CustomListSerializer : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var values = (IEnumerable)value;
var items = values.Cast<object>().ToList();
var s = JsonConvert.SerializeObject(items);
writer.WriteValue(s);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanConvert(Type objectType)
{
return objectType.IsAssignableFrom(typeof(IEnumerable));
}
}
[JsonConverter(typeof(CustomListSerializer))]
internal class CustomList<T> : List<T>
{
}
class Program
{
static void Main(string[] args)
{
var parameters = new Dictionary<string, object>();
parameters.Add("counting", new CustomList<int>() { 1, 2, 3, 5 });
parameters.Add("users", new CustomList<User>() { new User { Name = "TryingToImprove" }, new User { Name = "rubenvb" } });
parameters.Add("name", "Numbers");
parameters.Add("size", 4);
var thing = new
{
Parameters = parameters,
Name = "THING",
Test = new List<int>() { 1, 2, 3}
};
Console.WriteLine(JsonConvert.SerializeObject(thing));
}
}
internal class User
{
public string Name { get; set; }
}
which will return
{
"Parameters": {
"counting": "[1,2,3,5]",
"users": "[{\"Name\":\"TryingToImprove\"},{\"Name\":\"rubenvb\"}]",
"name": "Numbers",
"size": 4
},
"Name": "THING",
"Test": [1, 2, 3]
}
This is working -
internal class CustomJsonFormatter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType.IsAssignableFrom(typeof(Thing));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var data = value as Thing;
foreach (var prop in data.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
{
writer.WriteStartObject();
writer.WritePropertyName(prop.Name);
writer.WriteStartObject();
var local = prop.GetValue(data, null) as Dictionary<string, object>;
foreach (var key in local.Keys)
{
writer.WritePropertyName(key);
if (local[key].GetType() == typeof(List<int>))
{
string s = "[";
var arr = ((List<int>)local[key]);
for (var i = 0; i < arr.Count; i++)
{
s += arr[i].ToString() + ",";
}
writer.WriteValue(s.Substring(0, s.Length - 1) + "]");
}
else { writer.WriteValue(local[key]); }
}
}
writer.WriteEndObject();
writer.WriteEndObject();
}
var settings = new JsonSerializerSettings();
settings.Converters.Add(new CustomJsonFormatter());
string result = JsonConvert.SerializeObject(thing, settings);
produces
{"Parameters":{"counting":"[1,2,3]","name":"Numbers","size":3}}