Related
I created a JSON schema for my C# code using:
// Create JSON schema
var generator = new JSchemaGenerator();
var schema = generator.Generate(typeof(ConfigFileJsonSchema));
schema.Title = "PlexCleaner Schema";
schema.Description = "PlexCleaner config file JSON schema";
schema.SchemaVersion = new Uri("http://json-schema.org/draft-06/schema");
schema.Id = new Uri("https://raw.githubusercontent.com/ptr727/PlexCleaner/main/PlexCleaner.schema.json");
Console.WriteLine(schema);
I want to add a reference to this scheme whenever I create JSON output from my code:
private static string ToJson(ConfigFileJsonSchema settings)
{
return JsonConvert.SerializeObject(settings, Settings);
}
private static readonly JsonSerializerSettings Settings = new()
{
Formatting = Formatting.Indented,
StringEscapeHandling = StringEscapeHandling.EscapeNonAscii,
NullValueHandling = NullValueHandling.Ignore,
// We expect containers to be cleared before deserializing
// Make sure that collections are not read-only (get; set;) else deserialized values will be appended
// https://stackoverflow.com/questions/35482896/clear-collections-before-adding-items-when-populating-existing-objects
ObjectCreationHandling = ObjectCreationHandling.Replace
// TODO: Add TraceWriter to log to Serilog
};
How can I programmatically add the $schema URI to the created JSON, not meaning creating schema on the fly, but something like this:
public class ConfigFileJsonSchemaBase
{
// Schema reference
[JsonProperty(PropertyName = "$schema", Order = -2)]
public string Schema { get; } = "https://raw.githubusercontent.com/ptr727/PlexCleaner/main/PlexCleaner.schema.json";
// Default to 0 if no value specified, and always write the version first
[DefaultValue(0)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate, Order = -2)]
public int SchemaVersion { get; set; } = ConfigFileJsonSchema.Version;
}
Without needing to add a $schema entry to the class.
E.g. equivalent of:
schema.SchemaVersion = new Uri("http://json-schema.org/draft-06/schema");
There is a similar unanswered question: json serialization to refer schema
You can use a JsonConverter:
public class SchemaJsonConverter : JsonConverter
{
private readonly string _schemaUrl;
private readonly Type[] _types;
public SchemaJsonConverter(string schemaUrl, params Type[] types)
{
this._schemaUrl = schemaUrl;
this._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
{
var o = (JObject)t;
o.AddFirst(new JProperty("$Schema", this._schemaUrl));
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 this._types.Any(t => t == objectType);
}
}
You need the type to check the types affected by the converter and the schema url to inject it in your JSON. The converter allow you a fine control about the process of serialization.
I use a simple class to test the converter:
public class Something
{
public int Integer { get; set; }
public string Text { get; set; }
}
And a method to run the sample:
public static void Test()
{
var something = new Something
{
Integer = 37,
Text = "A text"
};
var settings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
StringEscapeHandling = StringEscapeHandling.EscapeNonAscii,
NullValueHandling = NullValueHandling.Ignore,
// We expect containers to be cleared before deserializing
// Make sure that collections are not read-only (get; set;) else deserialized values will be appended
// https://stackoverflow.com/questions/35482896/clear-collections-before-adding-items-when-populating-existing-objects
ObjectCreationHandling = ObjectCreationHandling.Replace
// TODO: Add TraceWriter to log to Serilog
};
var schemaUrl = "http://json-schema.org/draft-06/schema";
settings.Converters.Add(new SchemaJsonConverter(schemaUrl, something.GetType()));
var json = JsonConvert.SerializeObject(something, settings);
Console.WriteLine(json);
}
Output:
{
"$Schema": "http://json-schema.org/draft-06/schema",
"Integer": 37,
"Text": "A text"
}
UPDATE
A static method for serialization:
public static string SerializeJson(object obj, JsonSerializerSettings settings, string schemaUrl = null)
{
if (!string.IsNullOrEmpty(schemaUrl))
{
settings.Converters.Add(new SchemaJsonConverter(schemaUrl, obj.GetType()));
}
return JsonConvert.SerializeObject(obj, settings);
}
Usage:
var json = SerializeJson(something, settings, schemaUrl);
This Imgur api call returns a list containing both Gallery Image and Gallery Album classes represented in JSON.
I can't see how to deserialize these automatically using Json.NET given that there is no $type property telling the deserializer which class is meant to be represented. There is a property called "IsAlbum" that can be used to differentiate between the two.
This question appears to show one method but it looks like a bit of a hack.
How do I go about deserializing these classes? (using C#, Json.NET).
Sample Data:
Gallery Image
{
"id": "OUHDm",
"title": "My most recent drawing. Spent over 100 hours.",
...
"is_album": false
}
Gallery Album
{
"id": "lDRB2",
"title": "Imgur Office",
...
"is_album": true,
"images_count": 3,
"images": [
{
"id": "24nLu",
...
"link": "http://i.imgur.com/24nLu.jpg"
},
{
"id": "Ziz25",
...
"link": "http://i.imgur.com/Ziz25.jpg"
},
{
"id": "9tzW6",
...
"link": "http://i.imgur.com/9tzW6.jpg"
}
]
}
}
You can do this fairly easily by creating a custom JsonConverter to handle the object instantiation. Assuming you have your classes defined something like this:
public abstract class GalleryItem
{
public string id { get; set; }
public string title { get; set; }
public string link { get; set; }
public bool is_album { get; set; }
}
public class GalleryImage : GalleryItem
{
// ...
}
public class GalleryAlbum : GalleryItem
{
public int images_count { get; set; }
public List<GalleryImage> images { get; set; }
}
You would create the converter like this:
public class GalleryItemConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(GalleryItem).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader,
Type objectType, object existingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
// Using a nullable bool here in case "is_album" is not present on an item
bool? isAlbum = (bool?)jo["is_album"];
GalleryItem item;
if (isAlbum.GetValueOrDefault())
{
item = new GalleryAlbum();
}
else
{
item = new GalleryImage();
}
serializer.Populate(jo.CreateReader(), item);
return item;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer,
object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Here's an example program showing the converter in action:
class Program
{
static void Main(string[] args)
{
string json = #"
[
{
""id"": ""OUHDm"",
""title"": ""My most recent drawing. Spent over 100 hours."",
""link"": ""http://i.imgur.com/OUHDm.jpg"",
""is_album"": false
},
{
""id"": ""lDRB2"",
""title"": ""Imgur Office"",
""link"": ""http://alanbox.imgur.com/a/lDRB2"",
""is_album"": true,
""images_count"": 3,
""images"": [
{
""id"": ""24nLu"",
""link"": ""http://i.imgur.com/24nLu.jpg""
},
{
""id"": ""Ziz25"",
""link"": ""http://i.imgur.com/Ziz25.jpg""
},
{
""id"": ""9tzW6"",
""link"": ""http://i.imgur.com/9tzW6.jpg""
}
]
}
]";
List<GalleryItem> items =
JsonConvert.DeserializeObject<List<GalleryItem>>(json,
new GalleryItemConverter());
foreach (GalleryItem item in items)
{
Console.WriteLine("id: " + item.id);
Console.WriteLine("title: " + item.title);
Console.WriteLine("link: " + item.link);
if (item.is_album)
{
GalleryAlbum album = (GalleryAlbum)item;
Console.WriteLine("album images (" + album.images_count + "):");
foreach (GalleryImage image in album.images)
{
Console.WriteLine(" id: " + image.id);
Console.WriteLine(" link: " + image.link);
}
}
Console.WriteLine();
}
}
}
And here is the output of the above program:
id: OUHDm
title: My most recent drawing. Spent over 100 hours.
link: http://i.imgur.com/OUHDm.jpg
id: lDRB2
title: Imgur Office
link: http://alanbox.imgur.com/a/lDRB2
album images (3):
id: 24nLu
link: http://i.imgur.com/24nLu.jpg
id: Ziz25
link: http://i.imgur.com/Ziz25.jpg
id: 9tzW6
link: http://i.imgur.com/9tzW6.jpg
Fiddle: https://dotnetfiddle.net/1kplME
Simply with JsonSubTypes attributes that work with Json.NET
[JsonConverter(typeof(JsonSubtypes), "is_album")]
[JsonSubtypes.KnownSubType(typeof(GalleryAlbum), true)]
[JsonSubtypes.KnownSubType(typeof(GalleryImage), false)]
public abstract class GalleryItem
{
public string id { get; set; }
public string title { get; set; }
public string link { get; set; }
public bool is_album { get; set; }
}
public class GalleryImage : GalleryItem
{
// ...
}
public class GalleryAlbum : GalleryItem
{
public int images_count { get; set; }
public List<GalleryImage> images { get; set; }
}
Advanced to Brian Rogers answer. And about "use Serializer.Populate() instead of item.ToObject()".
If derived types has contstructors or some of their has own customconverter you must use general way for deserialize JSON.
So you must leave work for instantiate new object to NewtonJson. This way you can achieve it in you CustomJsonConverter:
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
..... YOU Code For Determine Real Type of Json Record .......
// 1. Correct ContractResolver for you derived type
var contract = serializer.ContractResolver.ResolveContract(DeterminedType);
if (converter != null && !typeDeserializer.Type.IsAbstract && converter.GetType() == GetType())
{
contract.Converter = null; // Clean Wrong Converter grabbed by DefaultContractResolver from you base class for derived class
}
// Deserialize in general way
var jTokenReader = new JTokenReader(jObject);
var result = serializer.Deserialize(jTokenReader, DeterminedType);
return (result);
}
This work if you have recursion of objects.
I'm only posting this to clear up some of the confusion. If you are working with a predefined format and need to deserialize it, this is what I found worked best and demonstrates the mechanics so that others can tweak it as needed.
public class BaseClassConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var j = JObject.Load(reader);
var retval = BaseClass.From(j, serializer);
return retval;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
public override bool CanConvert(Type objectType)
{
// important - do not cause subclasses to go through this converter
return objectType == typeof(BaseClass);
}
}
// important to not use attribute otherwise you'll infinite loop
public abstract class BaseClass
{
internal static Type[] Types = new Type[] {
typeof(Subclass1),
typeof(Subclass2),
typeof(Subclass3)
};
internal static Dictionary<string, Type> TypesByName = Types.ToDictionary(t => t.Name.Split('.').Last());
// type property based off of class name
[JsonProperty(PropertyName = "type", Required = Required.Always)]
public string JsonObjectType { get { return this.GetType().Name.Split('.').Last(); } set { } }
// convenience method to deserialize a JObject
public static new BaseClass From(JObject obj, JsonSerializer serializer)
{
// this is our object type property
var str = (string)obj["type"];
// we map using a dictionary, but you can do whatever you want
var type = TypesByName[str];
// important to pass serializer (and its settings) along
return obj.ToObject(type, serializer) as BaseClass;
}
// convenience method for deserialization
public static BaseClass Deserialize(JsonReader reader)
{
JsonSerializer ser = new JsonSerializer();
// important to add converter here
ser.Converters.Add(new BaseClassConverter());
return ser.Deserialize<BaseClass>(reader);
}
}
Following implementation should let you de-serialize without changing the way you have designed your classes and by using a field other than $type to decide what to de-serialize it into.
public class GalleryImageConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(GalleryImage) || objectType == typeof(GalleryAlbum));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
if (!CanConvert(objectType))
throw new InvalidDataException("Invalid type of object");
JObject jo = JObject.Load(reader);
// following is to avoid use of magic strings
var isAlbumPropertyName = ((MemberExpression)((Expression<Func<GalleryImage, bool>>)(s => s.is_album)).Body).Member.Name;
JToken jt;
if (!jo.TryGetValue(isAlbumPropertyName, StringComparison.InvariantCultureIgnoreCase, out jt))
{
return jo.ToObject<GalleryImage>();
}
var propValue = jt.Value<bool>();
if(propValue) {
resultType = typeof(GalleryAlbum);
}
else{
resultType = typeof(GalleryImage);
}
var resultObject = Convert.ChangeType(Activator.CreateInstance(resultType), resultType);
var objectProperties=resultType.GetProperties();
foreach (var objectProperty in objectProperties)
{
var propType = objectProperty.PropertyType;
var propName = objectProperty.Name;
var token = jo.GetValue(propName, StringComparison.InvariantCultureIgnoreCase);
if (token != null)
{
objectProperty.SetValue(resultObject,token.ToObject(propType)?? objectProperty.GetValue(resultObject));
}
}
return resultObject;
}
catch (Exception ex)
{
throw;
}
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
#ИгорьОрлов's answer works for when you have types that can only be instantiated directly by JSON.net (due to [JsonConstructor] and/or use of [JsonProperty] directly on constructor parameters. However overwriting contract.Converter = null does not work when JSON.net has already cached the converter to use.
(This wouldn't be an issue if JSON.NET used immutable types to indicate when data and configuration is no-longer mutable, le sigh)
In my case, I did this:
Implemented a custom JsonConverter<T> (where T is my DTO's base class).
Defined a DefaultContractResolver subclass that overrides ResolveContractConverter to return my custom JsonConverter for only the base class.
In detail, and by example:
Supposing I have these immutable DTOs that represent a remote file-system (so there' DirectoryDto and FileDto which both inherit FileSystemDto, just like how DirectoryInfo and FileInfo derive from System.IO.FileSystemInfo):
public enum DtoKind
{
None = 0,
File,
Directory
}
public abstract class FileSystemDto
{
protected FileSystemDto( String name, DtoKind kind )
{
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.Kind = kind;
}
[JsonProperty( "name" )]
public String Name { get; }
[JsonProperty( "kind" )]
public String Kind { get; }
}
public class FileDto : FileSystemDto
{
[JsonConstructor]
public FileDto(
[JsonProperty("name" )] String name,
[JsonProperty("length")] Int64 length,
[JsonProperty("kind") ] DtoKind kind
)
: base( name: name, kind: kind )
{
if( kind != DtoKind.File ) throw new InvalidOperationException( "blargh" );
this.Length = length;
}
[JsonProperty( "length" )]
public Int64 Length { get; }
}
public class DirectoryDto : FileSystemDto
{
[JsonConstructor]
public FileDto(
[JsonProperty("name")] String name,
[JsonProperty("kind")] DtoKind kind
)
: base( name: name, kind: kind )
{
if( kind != DtoKind.Directory ) throw new InvalidOperationException( "blargh" );
}
}
Supposing I have a JSON array of FileSystemDto:
[
{ "name": "foo.txt", "kind": "File", "length": 12345 },
{ "name": "bar.txt", "kind": "File", "length": 12345 },
{ "name": "subdir", "kind": "Directory" },
]
I want Json.net to deserialize this to List<FileSystemDto>...
So define a subclass of DefaultContractResolver (or if you already have a resolver implementation then subclass (or compose) that) and override ResolveContractConverter:
public class MyContractResolver : DefaultContractResolver
{
protected override JsonConverter? ResolveContractConverter( Type objectType )
{
if( objectType == typeof(FileSystemDto) )
{
return MyJsonConverter.Instance;
}
else if( objectType == typeof(FileDto ) )
{
// use default
}
else if( objectType == typeof(DirectoryDto) )
{
// use default
}
return base.ResolveContractConverter( objectType );
}
}
Then implement MyJsonConverter:
public class MyJsonConverter : JsonConverter<FileSystemDto>
{
public static MyJsonConverter Instance { get; } = new MyJsonConverter();
private MyJsonConverter() {}
// TODO: Override `CanWrite => false` and `WriteJson { throw; }` if you like.
public override FileSystemDto? ReadJson( JsonReader reader, Type objectType, FileSystemDto? existingValue, Boolean hasExistingValue, JsonSerializer serializer )
{
if( reader.TokenType == JsonToken.Null ) return null;
if( objectType == typeof(FileSystemDto) )
{
JObject jsonObject = JObject.Load( reader );
if( jsonObject.Property( "kind" )?.Value is JValue jv && jv.Value is String kind )
{
if( kind == "File" )
{
return jsonObject.ToObject<FileDto>( serializer );
}
else if( kind == "Directory" )
{
return jsonObject.ToObject<DirectoryDto>( serializer );
}
}
}
return null; // or throw, depending on your strictness.
}
}
Then, to deserialize, use a JsonSerializer instance with the ContractResolver set correctly, for example:
public static IReadOnlyList<FileSystemDto> DeserializeFileSystemJsonArray( String json )
{
JsonSerializer jss = new JsonSerializer()
{
ContractResolver = new KuduDtoContractResolver()
};
using( StringReader strRdr = new StringReader( json ) )
using( JsonTextReader jsonRdr = new JsonTextReader( strRdr ) )
{
List<FileSystemDto>? list = jss.Deserialize< List<FileSystemDto> >( jsonRdr );
// TODO: Throw if `list` is null.
return list;
}
}
I am receiving data that looks like this from an online service provider:
{
name: "test data",
data: [
[ "2017-05-31", 2388.33 ],
[ "2017-04-30", 2358.84 ],
[ "2017-03-31", 2366.82 ],
[ "2017-02-28", 2329.91 ]
],
}
I would like to parse it into an object that looks like this:
public class TestData
{
public string Name;
public List<Tuple<DateTime, double>> Data;
}
The only thing I have been able to find is how to parse an array of objects into a list of tulples, for example: Json.NET deserialization of Tuple<...> inside another type doesn't work?
Is there a way to write a custom converter that would handle this?
If anyone is interested in a more generic solution for ValueTuples
public class TupleConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var type = value.GetType();
var array = new List<object>();
FieldInfo fieldInfo;
var i = 1;
while ((fieldInfo = type.GetField($"Item{i++}")) != null)
array.Add(fieldInfo.GetValue(value));
serializer.Serialize(writer, array);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var argTypes = objectType.GetGenericArguments();
var array = serializer.Deserialize<JArray>(reader);
var items = array.Select((a, index) => a.ToObject(argTypes[index])).ToArray();
var constructor = objectType.GetConstructor(argTypes);
return constructor.Invoke(items);
}
public override bool CanConvert(Type type)
{
return type.Name.StartsWith("ValueTuple`");
}
}
Usage is as follows:
var settings = new JsonSerializerSettings();
settings.Converters.Add(new TupleConverter());
var list = new List<(DateTime, double)>
{
(DateTime.Now, 7.5)
};
var json = JsonConvert.SerializeObject(list, settings);
var result = JsonConvert.DeserializeObject(json, list.GetType(), settings);
Rather than use tuples, I would create a class that is specific to the task. In this case your JSON data comes in as a list of lists of strings which is a bit awkward to deal with. One method would be to deserialise as List<List<string>> and then convert afterwards. For example, I would go with 3 classes like this:
public class IntermediateTestData
{
public string Name;
public List<List<string>> Data;
}
public class TestData
{
public string Name;
public IEnumerable<TestDataItem> Data;
}
public class TestDataItem
{
public DateTime Date { get; set; }
public double Value { get; set; }
}
Now deserialise like this:
var intermediate = JsonConvert.DeserializeObject<IntermediateTestData>(json);
var testData = new TestData
{
Name = intermediate.Name,
Data = intermediate.Data.Select(d => new TestDataItem
{
Date = DateTime.Parse(d[0]),
Value = double.Parse(d[1])
})
};
So using JSON.NET LINQ, I managed to get it to work as you prescribed...
var result = JsonConvert.DeserializeObject<JObject>(json);
var data = new TestData
{
Name = (string)result["name"],
Data = result["data"]
.Select(t => new Tuple<DateTime, double>(DateTime.Parse((string)t[0]), (double)t[1]))
.ToList()
};
This is the full test I wrote
public class TestData
{
public string Name;
public List<Tuple<DateTime, double>> Data;
}
[TestMethod]
public void TestMethod1()
{
var json =
#"{
name: ""test data"",
data: [
[ ""2017-05-31"", 2388.33 ],
[ ""2017-04-30"", 2358.84 ],
[ ""2017-03-31"", 2366.82 ],
[ ""2017-02-28"", 2329.91 ]
],
}";
var result = JsonConvert.DeserializeObject<JObject>(json);
var data = new TestData
{
Name = (string)result["name"],
Data = result["data"]
.Select(t => new Tuple<DateTime, double>(DateTime.Parse((string)t[0]), (double)t[1]))
.ToList()
};
Assert.AreEqual(2388.33, data.Data[0].Item2);
}
However, while this may work, I am in agreement with the rest of the comments/answers that using tuples for this is probably not the correct way to go. Using concrete POCO's is definitely going to be a hell of a lot more maintainable in the long run simply because of the Item1 and Item2 properties of the Tuple<,>.
They are not the most descriptive...
I took the generic TupleConverter from here: Json.NET deserialization of Tuple<...> inside another type doesn't work?
And made a generic TupleListConverter.
Usage:
public class TestData
{
public string Name;
[Newtonsoft.Json.JsonConverter(typeof(TupleListConverter<DateTime, double>))]
public List<Tuple<DateTime, double>> Data;
}
public void Test(string json)
{
var testData = JsonConvert.DeserializeObject<TestData>(json);
foreach (var tuple in testData.data)
{
var dateTime = tuple.Item1;
var price = tuple.Item2;
... do something...
}
}
Converter:
public class TupleListConverter<U, V> : Newtonsoft.Json.JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Tuple<U, V>) == objectType;
}
public override object ReadJson(
Newtonsoft.Json.JsonReader reader,
Type objectType,
object existingValue,
Newtonsoft.Json.JsonSerializer serializer)
{
if (reader.TokenType == Newtonsoft.Json.JsonToken.Null)
return null;
var jArray = Newtonsoft.Json.Linq.JArray.Load(reader);
var target = new List<Tuple<U, V>>();
foreach (var childJArray in jArray.Children<Newtonsoft.Json.Linq.JArray>())
{
var tuple = new Tuple<U, V>(
childJArray[0].ToObject<U>(),
childJArray[1].ToObject<V>()
);
target.Add(tuple);
}
return target;
}
public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
}
I am running into a strange problem with serializing data with json.net. Mainly, I am trying to rename the 'Key' and 'Value' names in the outgoing json to be something more descriptive. Specifically, I want the IRequest related 'Key' to be called 'Request' and the IQuoteTimeSeries 'Value' to be 'DataSeries'
Note, this will not be deserialized. It is only used in data analysis on the web page.
The data repository object I am serializing is a Dictionary<IRequest, IQuoteTimeSeries> object. The IRequest represents a specific request for data and the IQuoteTimeSeries is the object that contains the returning data as a SortedDictionary<DateTime, IQuote>. This is a series of data sorted by timestamp. In this example, I only have one item in the TimeSeries for brevity, but in most cases there would be many items.
Everything needs to be organized together, serialized and sent out to be consumed by JavaScript.
Here is the basic code for these objects;
[JsonArray]
public class QuoteRepository : Dictionary<IRequest, IQuoteTimeSeries>, IQuoteRepository
{
public QuoteRepository() { }
public void AddRequest(IRequest request)
{
if (!this.ContainsKey(request))
{
IQuoteTimeSeries tSeries = new QuoteTimeSeries(request);
this.Add(request, tSeries);
}
}
public void AddQuote(IRequest request, IQuote quote)
{
if (!this.ContainsKey(request))
{
QuoteTimeSeries tSeries = new QuoteTimeSeries(request);
this.Add(request, tSeries);
}
this[request].AddQuote(quote);
}
IEnumerator<IQuoteTimeSeries> Enumerable<IQuoteTimeSeries>.GetEnumerator()
{
return this.Values.GetEnumerator();
}
}
A quote time series looks like this;
[JsonArray]
public class QuoteTimeSeries: SortedDictionary<DateTime, IQuote>, IQuoteTimeSeries
{
public QuoteTimeSeries(IRequest request)
{
Request = request;
}
public IRequest Request { get; }
public void AddQuote(IQuote quote)
{
this[quote.QuoteTimeStamp] = quote;
}
public void MergeQuotes(IQuoteTimeSeries quotes)
{
foreach (IQuote item in quotes)
{
this[item.QuoteTimeStamp] = item;
}
}
IEnumerator<IQuote> IEnumerable<IQuote>.GetEnumerator()
{
return this.Values.GetEnumerator();
}
}
The code used to serialize this is fairly simple:
IQuoteRepository quotes = await requests.GetDataAsync();
JsonSerializerSettings settings = new JsonSerializerSettings
{
ContractResolver = QuoteRepositoryContractResolver.Instance,
NullValueHandling = NullValueHandling.Ignore
};
return Json<IQuoteRepository>(quotes, settings);
I added a contract resolver with the intention of overriding the property writing. The property.PropertyName = code is being hit and the property names are being changed, but the output JSON is unaffected.
public class QuoteRepositoryContractResolver : DefaultContractResolver
{
public static readonly QuoteRepositoryContractResolver Instance = new QuoteRepositoryContractResolver();
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
if (property.DeclaringType == typeof(KeyValuePair<IRequest, IQuoteTimeSeries>))
{
if (property.PropertyName.Equals("Key", StringComparison.OrdinalIgnoreCase))
{
property.PropertyName = "Request";
}
else if (property.PropertyName.Equals("Value", StringComparison.OrdinalIgnoreCase))
{
property.PropertyName = "Data";
}
}
else if (property.DeclaringType == typeof(KeyValuePair<DateTime, IQuote>))
{
if (property.PropertyName.Equals("Key", StringComparison.OrdinalIgnoreCase))
{
property.PropertyName = "TimeStamp";
}
else if (property.PropertyName.Equals("Value", StringComparison.OrdinalIgnoreCase))
{
property.PropertyName = "Quote";
}
}
return property;
}
}
The output JSON is odd. The Key and Value items are completely unchanged, even though I did change their names in the code.
[
{
"Key": {
"QuoteDate": "2016-05-12T00:00:00-04:00",
"QuoteType": "Index",
"Symbol": "SPY",
"UseCache": true
},
"Value": [
{
"Key": "2016-05-11T16:00:01-04:00",
"Value": {
"Description": "SPDR S&P 500",
"High": 208.54,
"Low": 206.50,
"Change": -1.95,
"ChangePer": -0.94,
"Price": 206.50,
"QuoteTimeStamp": "2016-05-11T16:00:01-04:00",
"Symbol": "SPY"
}
}
]
},
{
"Key": {
"QuoteDate": "2016-05-12T00:00:00-04:00",
"QuoteType": "Stock",
"Symbol": "GOOG",
"UseCache": true
},
"Value": [
{
"Key": "2016-05-11T16:00:00-04:00",
"Value": {
"Description": "Alphabet Inc.",
"High": 724.48,
"Low": 712.80,
"Change": -7.89,
"ChangePer": -1.09,
"Price": 715.29,
"QuoteTimeStamp": "2016-05-11T16:00:00-04:00",
"Symbol": "GOOG"
}
}
]
}
]
Does anyone know how to change the 'Key' and 'Value' items properly?
One way to solve this is to use a custom JsonConverter for your dictionary-based classes. The code is actually pretty straightforward.
public class CustomDictionaryConverter<K, V> : JsonConverter
{
private string KeyPropertyName { get; set; }
private string ValuePropertyName { get; set; }
public CustomDictionaryConverter(string keyPropertyName, string valuePropertyName)
{
KeyPropertyName = keyPropertyName;
ValuePropertyName = valuePropertyName;
}
public override bool CanConvert(Type objectType)
{
return typeof(IDictionary<K, V>).IsAssignableFrom(objectType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
IDictionary<K, V> dict = (IDictionary<K, V>)value;
JArray array = new JArray();
foreach (var kvp in dict)
{
JObject obj = new JObject();
obj.Add(KeyPropertyName, JToken.FromObject(kvp.Key, serializer));
obj.Add(ValuePropertyName, JToken.FromObject(kvp.Value, serializer));
array.Add(obj);
}
array.WriteTo(writer);
}
public override bool CanRead
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
When it is time to serialize, add the converters to the JsonSerializerSettings like this:
JsonSerializerSettings settings = new JsonSerializerSettings
{
Converters = new List<JsonConverter>
{
new CustomDictionaryConverter<IRequest, IQuoteTimeSeries>("Request", "Data"),
new CustomDictionaryConverter<DateTime, IQuote>("TimeStamp", "Quote")
},
Formatting = Formatting.Indented
};
string json = JsonConvert.SerializeObject(repo, settings);
Fiddle: https://dotnetfiddle.net/roHEtx
Converting your dictionary from Dictionary<IRequest, IQuoteTimeSeries> to List<KeyValuePair<IRequest, IQuoteTimeSeries>> should also work.
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}}