I'm trying to use the System.Text.Json.JsonSerializer to deserialize the model partially, so one of the properties is read as string that contains the original JSON.
public class SomeModel
{
public int Id { get; set; }
public string Name { get; set; }
public string Info { get; set; }
}
The example code
var json = #"{
""Id"": 1,
""Name"": ""Some Name"",
""Info"": {
""Additional"": ""Fields"",
""Are"": ""Inside""
}
}";
var model = JsonSerializer.Deserialize<SomeModel>(json);
should produce the model, which Info property contains the Info object from the original JSON as string:
{
"Additional": "Fields",
"Are": "Inside"
}
It doesn't work out of the box and throws an exception:
System.Text.Json.JsonException: ---> System.InvalidOperationException:
Cannot get the value of a token type 'StartObject' as a string.
What have I tried so far:
public class InfoToStringConverter : JsonConverter<string>
{
public override string Read(
ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
{
return reader.GetString();
}
public override void Write(
Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
and apply it in the model as
[JsonConverter(typeof(InfoToStringConverter))]
public string Info { get; set; }
and add in the options to JsonSerializer
var options = new JsonSerializerOptions();
options.Converters.Add(new InfoToStringConverter());
var model = JsonSerializer.Deserialize<SomeModel>(json, options);
Still, it throws the same exception:
System.Text.Json.JsonException: ---> System.InvalidOperationException:
Cannot get the value of a token type 'StartObject' as a string.
What is the right recipe to cook what I need? It worked in a similar way using Newtonsoft.Json.
Update
For me it is important to keep the nested JSON object as original as possible. So, I'd avoid options like to deserialize as Dictionary and serialize back, because I'm afraid to introduce undesirable changes.
Found a right way how to correctly read the nested JSON object inside the JsonConverter. The complete solution is the following:
public class SomeModel
{
public int Id { get; set; }
public string Name { get; set; }
[JsonConverter(typeof(InfoToStringConverter))]
public string Info { get; set; }
}
public class InfoToStringConverter : JsonConverter<string>
{
public override string Read(
ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using (var jsonDoc = JsonDocument.ParseValue(ref reader))
{
return jsonDoc.RootElement.GetRawText();
}
}
public override void Write(
Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
In the code itself there is no need even to create options:
var json = #"{
""Id"": 1,
""Name"": ""Some Name"",
""Info"": {
""Additional"": ""Fields"",
""Are"": ""Inside""
}
}";
var model = JsonSerializer.Deserialize<SomeModel>(json);
The raw JSON text in the Info property contains even extra spaces introduced in the example for nice readability.
And there is no mixing of model representation and its serialization as remarked #PavelAnikhouski in his answer.
You can use a JsonExtensionData attribute for that and declare a Dictionary<string, JsonElement> or Dictionary<string, object> property in your model to store this information
public class SomeModel
{
public int Id { get; set; }
public string Name { get; set; }
[JsonExtensionData]
public Dictionary<string, JsonElement> ExtensionData { get; set; }
[JsonIgnore]
public string Data
{
get
{
return ExtensionData?["Info"].GetRawText();
}
}
}
Then you can add an additional property to get a string from this dictionary by Info key. In code above the Data property will contain the expected string
{
"Additional": "Fields",
"Are": "Inside"
}
For some reasons adding the property with the same name Info doesn't work, even with JsonIgnore. Have a look at Handle overflow JSON for details.
You can also declare the Info property as JsonElement type and get raw text from it
public class SomeModel
{
public int Id { get; set; }
public string Name { get; set; }
public JsonElement Info { get; set; }
}
var model = JsonSerializer.Deserialize<SomeModel>(json);
var rawString = model.Info.GetRawText();
But it will cause a mixing of model representation and its serialization.
Another option is to parse the data using JsonDocument, enumerate properties and parse them one by one, like that
var document = JsonDocument.Parse(json);
foreach (var token in document.RootElement.EnumerateObject())
{
if (token.Value.ValueKind == JsonValueKind.Number)
{
if(token.Value.TryGetInt32(out int number))
{
}
}
if (token.Value.ValueKind == JsonValueKind.String)
{
var stringValue = token.Value.GetString();
}
if (token.Value.ValueKind == JsonValueKind.Object)
{
var rawContent = token.Value.GetRawText();
}
}
a quick addendum to the accepted answer:
If you need to write raw JSON values as well, here is an implementation of the Write method for the converter:
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
using (JsonDocument document = JsonDocument.Parse(value))
{
document.RootElement.WriteTo(writer);
}
}
As outlined in the dotnet runtime repo on github, this seems to be the "proper" way to workaround the fact that they decided not to implement a WriteRawValue method.
Related
So I got this guy
{
"lng_x": "106.883368",
"style": "{\"name\":\"TACTICAL\"}"
}
Which I wanted to make into this guy
public class Root
{
public string lng_x { get; set; }
public Style style { get; set; }
}
public class Style
{
public string name { get; set; }
}
But I got this instead
public class Root
{
public string lng_x { get; set; }
public string style { get; set; }
}
I know that the Json is supposed to be like this
{
"lng_x": "106.883368",
"style":
{
"name": "TACTICAL"
}
}
But I can't because it is already like that when I got it.
Is there any way so that I don't have to deserialize it again?
If you can't fix the JSON document you can create a custom JSON type converter and apply it to the style property. And whoever created that document needs to fix their bug.
If you use System.Text.Json, a possible converter could be :
public class StyleStringJsonConverter : JsonConverter<Style>
{
public override Style Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options) =>
JsonSerializer.Deserialize<Style>(reader.GetString()!);
public override void Write(
Utf8JsonWriter writer,
Style style,
JsonSerializerOptions options) =>
writer.WriteStringValue(JsonSerializer.Serialize(style));
}
This can be applied through an attribute :
public class Root
{
public string lng_x { get; set; }
[JsonConverter(typeof(StyleStringJsonConverter))]
public Style style { get; set; }
}
JSON.NET also has custom converters:
public class StyleStringConverter : JsonConverter<Style>
{
public override void WriteJson(JsonWriter writer, Style value, JsonSerializer serializer)
{
writer.WriteValue(JsonConvert.SerializeObject(value));
}
public override Style ReadJson(JsonReader reader, Type objectType, Style existingValue, bool hasExistingValue, JsonSerializer serializer)
{
string s = (string)reader.Value;
return JsonConvert.DeserializeObject<Style>(s);
}
}
This can be applied using an attribute too:
public class Root
{
public string lng_x { get; set; }
[JsonConverter(typeof(StyleStringJsonConverter))]
public Style style { get; set; }
}
As stated before, the original creators should fix your the JSON file.
Meanwhile, you are stuck with it.
To make sure you don't dirty your own code
use Regex to make it a real JSON
Deserialize as normal
Advantage: When the JSON is changed to a correct form you have to remove the CleanJSON Method and your code will stay working.
The Code
using System.Text.Json;
using System.Text.RegularExpressions;
function YourFunction(){
//-- Get your JSON in string format (from API, file,...)
string _jsonString = ... ;
CleanJSON(_jsonString);
YourModels.Root _correctStructure = (JsonSerializer.Deserialize<YourModels.Root>(_JSsonString);
}
function CleanJSON(string jsonString){
//__ Probably you can group the regex
string _regexPatternRoot = #"(\\"")(.*?)(\\)";
string _regexReplacementRoot = #"""$2";
string _regexPatternStyle = "\"({)\"(.*?)\"}\"";
string _regexReplacementStyle = #"$1""$2""}";
_JSsonString = Regex.Replace(_JSsonString, _regexPatternRoot, _regexReplacementRoot);
_JSsonString = Regex.Replace(_JSsonString, _regexPatternStyle, _regexReplacementStyle);
}
you don't need any custom converter. The problem is that your origional style property is a json string and you have to deserialize it. In this case I usually use a JsonConstructor
public class Root
{
public string lng_x { get; set; }
public Style style { get; set; }
[JsonConstructor]
public Root (string style)
{
this.style=JsonConvert.DeserializeObject<Style>(style);
}
public Root (){}
}
test
Root data = JsonConvert.DeserializeObject<Root>(json);
{
"lng_x": "106.883368",
"style": {
"name": "TACTICAL"
}
}
I get data from an API in this form:
[
1234,
{
"a": [
[
"5541.30000",
"2.50700000",
"1534614248.456738",
"r"
],
[
"5542.50000",
"0.40100000",
"1534614248.456738",
"r"
]
],
"c": "974942666"
},
"book-25",
"XBT/USD"
]
The order of the fields in this array data dictates what field in my C# class it maps to. The elements have different types so can't be mapped to a single List or array as far as I know. I want deserialise this into the following class structure:
public class MessageResponseBookSnapshot: MessageResponseBase
{
public int ChannelID { get; set; }
public BookSnapshot Book { get; set; }
public string ChannelName { get; set; }
public string Pair { get; set; }
}
public class BookSnapshot
{
public OrderbookLevel[] As { get; set; }
public OrderbookLevel[] Bs { get; set; }
}
public class OrderbookLevel
{
public decimal Price { get; set; }
public decimal Volume { get; set; }
public decimal Timestamp { get; set; }
}
Note there are some difference between the JSON and the C# classes, this is information that is not relevant to me so I discard it. If it makes deserialisation easier to include these in the class I am happy to do that. I cannot change the format in which I receive the data.
The problem is I don't know how to get the JSON serialiser to automatically recognise the mappings, eg. that the first element of the array maps to ChannelID. Instead I've implemented the serialisation myself like this:
private static MessageResponseBase DeserialiseBookUpdate(string message)
{
using (JsonDocument document = JsonDocument.Parse(message))
{
var deserialisedMessage = new MessageResponseBookSnapshot();
JsonElement root = document.RootElement;
var arrayEnumerator = root.EnumerateArray();
arrayEnumerator.MoveNext();
deserialisedMessage.ChannelID = arrayEnumerator.Current.GetInt32();
var bookSnapshot = new BookSnapshot();
...cut for brevity...
deserialisedMessage.Book = bookSnapshot;
return deserialisedMessage;
}
}
This is very cumbersome, ideally I would be looking for some kind of attribute that can be applied to the fields of the class. Is there a better way to this so that the JSON serialiser can find the mappings to my data structures, instead of having to manually map them myself?
EDIT:
I wanted to highlight as well that some of the fields don't map to their type, so for example, the JSON contains decimal numbers inside quotes, which in JSON would be of type string but I call decimal.Parse() on these after using GetString(). For this question, it's not necessary for them to be mapped to decimals, strings would be fine.
For a more generic approach, you could create a custom JsonConverter that uses custom attributes to identify which types should be converted from a JSON array and which order the properties should be converted in.
First, let's define the attributes we'll use:
// Identifies types that should be converted from a JSON array
public class FromJsonArrayAttribute : Attribute { }
// Used to choose which order the properties should be converted in
public class PropertyIndexAttribute : Attribute
{
public int Index { get; set; }
public PropertyIndexAttribute(int index)
{
Index = index;
}
}
The JsonConverter would look something like the following. Note, that this is a rough version that is just made to handle the example in the question, but should be applicable in most places. You might however want to adjust a bit if for example you need to be able to convert more than just public properties or if null can appear in certain places. Also, the implementation currently can only deserialize and not serialize as you can see:
public class ArrayToObjectConverter<T> : JsonConverter<T>
{
public ArrayToObjectConverter(JsonSerializerOptions options)
{ }
public override bool CanConvert(Type typeToConvert)
{
// The type we're converting to has a [FromJsonArray] attribute
return Attribute.IsDefined(typeToConvert, typeof(FromJsonArrayAttribute));
}
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException($"Expected the start of a JSON array but found TokenType {reader.TokenType}");
}
var propMap = typeToConvert.GetProperties() // Only public properties
.Where(prop => Attribute.IsDefined(prop, typeof(PropertyIndexAttribute)))
.ToDictionary(
prop => prop.GetCustomAttribute<PropertyIndexAttribute>().Index,
prop => prop);
var result = Activator.CreateInstance(typeToConvert);
var index = 0;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
return (T)result;
}
else if (propMap.TryGetValue(index, out var prop))
{
var value = JsonSerializer.Deserialize(
ref reader,
prop.PropertyType,
options);
prop.SetValue(result, value);
}
else if (reader.TokenType == JsonTokenType.StartObject)
{
// Skip this whole object, as the target type
// has no matching property for this object
reader.Skip();
}
index++;
}
return (T)result;
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
Because the converter should be able to handle more than one type (it uses a generic type parameter), we need to use a JsonConverterFactory
public class ArrayToObjectConverterFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
{
return Attribute.IsDefined(typeToConvert, typeof(FromJsonArrayAttribute));
}
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
{
JsonConverter converter = (JsonConverter)Activator.CreateInstance(
typeof(ArrayToObjectConverter<>).MakeGenericType(
new Type[] { type }),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: new object[] { options },
culture: null);
return converter;
}
}
Now we need to annotate our types:
[FromJsonArray]
public class MessageResponseBookSnapshot
{
[PropertyIndex(0)]
public int ChannelID { get; set; }
[PropertyIndex(1)]
public BookSnapshot Book { get; set; }
[PropertyIndex(2)]
public string ChannelName { get; set; }
[PropertyIndex(3)]
public string Pair { get; set; }
}
public class BookSnapshot
{
[JsonPropertyName("a")]
public OrderbookLevel[] As { get; set; }
[JsonPropertyName("b")]
public OrderbookLevel[] Bs { get; set; }
}
[FromJsonArray]
public class OrderbookLevel
{
[PropertyIndex(0)]
public decimal Price { get; set; }
[PropertyIndex(1)]
public decimal Volume { get; set; }
[PropertyIndex(2)]
public decimal Timestamp { get; set; }
}
And as a little bonus, let's handle converting strings to decimals:
public class DecimalConverter : JsonConverter<decimal>
{
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(decimal);
}
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> reader.TokenType switch
{
JsonTokenType.Number => reader.GetDecimal(),
JsonTokenType.String => decimal.Parse(reader.GetString()),
_ => throw new JsonException($"Expected a Number or String but got TokenType {reader.TokenType}")
};
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
{
throw new NotImplementedException();
}
}
And lastly, let's do some deserialization:
var options = new JsonSerializerOptions();
options.Converters.Add(new ArrayToObjectConverterFactory());
options.Converters.Add(new DecimalConverter());
var result = JsonSerializer.Deserialize<MessageResponseBookSnapshot>(json, options);
More information on custom converters can be found in the documentation.
A working example can be found in this fiddle.
I have a very similar issue to this question here, except my application is in C#, and I can't figure out how to convert the solution unfortunately. I am trying to deserialize a JSON result that looks like this:
"error":[],
"result":
{
"MANAEUR":[
[1619042400,"1.11200","1.13488","1.08341","1.10077","1.09896","58878.56534370",137],
[1619046000,"1.09767","1.12276","1.08490","1.11097","1.10456","25343.25910419",77],
],
"last":1619118000
}
I use the following classes:
public class ResponseBase
{
[JsonProperty(PropertyName = "error")]
public List<string> Error;
}
public class OHLCResponse : ResponseBase
{
[JsonProperty("result")]
public OHLCResult Result;
}
public class OHLCResult
{
[JsonProperty("pair_names")]
public Dictionary<string, OHLC[]> GetHistory;
[JsonProperty("last")]
public long Last;
}
.... and then finally the guts of it:
public class OHLC
{
public int Time;
public decimal Open;
public decimal High;
public decimal Low;
public decimal Close;
public decimal Vwap;
public decimal Volume;
public int Count;
}
I have a standard deserializer class which works for all other calls I am using to the same API, but I cannot get this call to work. When I retrieve OHLCResponse object,I don't get an error, and "Result.Last" is always populated, but the expected array of OHLC items in "Result.GetHistory" is always empty/null. I know that the data has been returned successfully since I can see the data in the variable returned from the WebRequest that I am then passing to the deserializer function, so it must be that I have these classes laid out incorrectly I guess.
Can anyone see what I'm doing wrong?
Many thanks in advance, Dave
The object you posted isn't valid JSON. The outside curly braces are missing. So I am going to assume it should look like this:
{
"error": [],
"result": {
"MANAEUR": [
[1619042400, "1.11200", "1.13488", "1.08341", "1.10077", "1.09896", "58878.56534370", 137],
[1619046000, "1.09767", "1.12276", "1.08490", "1.11097", "1.10456", "25343.25910419", 77],
],
"last": 1619118000
}
}
Anonymous Deserialization
The first method you could do, which may be a tad kludgy since you have to deserialize twice, is use anonymous deserialization.
Let's start by defining some models:
public sealed class OHLCModel
{
public long Time { get; set; }
public decimal Open { get; set; }
public decimal High { get; set; }
public decimal Low { get; set; }
public decimal Close { get; set; }
public decimal Vwap { get; set; }
public decimal Volume { get; set; }
public int Count { get; set; }
}
public sealed class ResultModel
{
[JsonIgnore]
public IEnumerable<OHLCModel> Manaeur { get; set; }
[JsonProperty("last")]
public long Last { get; set; }
}
public sealed class RootModel
{
[JsonProperty("error")]
public List<string> Error { get; set; }
[JsonProperty("result")]
public ResultModel Result { get; set; }
}
As you can see we are ignoring the Manaeur object when serialization happens.
To make this method work, we'd do this:
var json = System.IO.File.ReadAllText(#"c:\users\andy\desktop\test.json");
// First, just grab the object that has the mixed arrays.
// This creates a "template" of the format of the target object
var dto = JsonConvert.DeserializeAnonymousType(json, new
{
Result = new
{
Manaeur = new List<List<object>>()
}
});
// Next, deserialize the rest of it
var fullObject = JsonConvert.DeserializeObject<RootModel>(json);
// transfer the DTO using a Select statement
fullObject.Result.Manaeur = dto.Result.Manaeur.Select(x => new OHLCModel
{
Time = Convert.ToInt64(x[0]),
Open = Convert.ToDecimal(x[1]),
High = Convert.ToDecimal(x[2]),
Low = Convert.ToDecimal(x[3]),
Close = Convert.ToDecimal(x[4]),
Vwap = Convert.ToDecimal(x[5]),
Volume = Convert.ToDecimal(x[6]),
Count = Convert.ToInt32(x[7])
});
This isn't the most ideal solution as you are tightly coupling to the model in a few spots. The ideal way to do this would be to make a custom JsonSerializer.
Use a Custom JsonConverter
First thing we do is change your ResultModel to look like this:
public sealed class ResultModel
{
[JsonConverter(typeof(ManaeurJsonConverter)), JsonProperty("MANAEUR")]
public IEnumerable<OHLCModel> Manaeur { get; set; }
[JsonProperty("last")]
public long Last { get; set; }
}
Then implement a JsonConverter:
public sealed class ManaeurJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => false; // this will never get called
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var lst = JArray.Load(reader).ToObject<List<List<object>>>();
return lst.Select(x => new OHLCModel
{
Time = Convert.ToInt64(x[0]),
Open = Convert.ToDecimal(x[1]),
High = Convert.ToDecimal(x[2]),
Low = Convert.ToDecimal(x[3]),
Close = Convert.ToDecimal(x[4]),
Vwap = Convert.ToDecimal(x[5]),
Volume = Convert.ToDecimal(x[6]),
Count = Convert.ToInt32(x[7])
});
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{ // we don't need to write
throw new NotImplementedException();
}
}
You can then simply call it as so:
var json = System.IO.File.ReadAllText(#"c:\users\andy\desktop\test.json");
var fullObject = JsonConvert.DeserializeObject<RootModel>(json);
Your JSON is not valid, I had to modify it to make it valid JSON (https://jsonformatter.org/). I added the root brackets and removed the comma delimter after the second inner array entry.
Valid JSON:
{
"error":[],
"result":
{
"MANAEUR":[
[1619042400,"1.11200","1.13488","1.08341","1.10077","1.09896","58878.56534370",137],
[1619046000,"1.09767","1.12276","1.08490","1.11097","1.10456","25343.25910419",77]
],
"last":1619118000
}
}
After updating the JSON, I used Visual Studio's 'Paste Special' to generate C# objects from the JSON. The following classes were created.
public class RootObject
{
[JsonProperty("error")]
public object[] Error { get; set; }
[JsonProperty("result")]
public Result Result { get; set; }
}
public class Result
{
[JsonProperty("MANAEUR")]
public object[][] Manaeur { get; set; }
[JsonProperty("last")]
public int Last { get; set; }
}
With the above JSON and classes, I used the following to deserialize the JSON.
string json = "{\"error\":[],\"result\":{\"MANAEUR\":[[1619042400,\"1.11200\",\"1.13488\",\"1.08341\",\"1.10077\",\"1.09896\",\"58878.56534370\",137],[1619046000,\"1.09767\",\"1.12276\",\"1.08490\",\"1.11097\",\"1.10456\",\"25343.25910419\",77]],\"last\":1619118000}}";
var obj = JsonConvert.DeserializeObject<RootObject>(json);
EDIT:
To handle the MANAEUR property where the key label can be different.
Create a JsonConverter...
public class ManaeurConverter : JsonConverter
{
private Dictionary<string, string> propertyMappings { get; set; }
public ManaeurConverter()
{
this.propertyMappings = new Dictionary<string, string>
{
{"NOTMANAEUR","MANAEUR"}
};
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
object instance = Activator.CreateInstance(objectType);
var props = objectType.GetTypeInfo().DeclaredProperties.ToList();
JObject jo = JObject.Load(reader);
foreach (JProperty jp in jo.Properties())
{
if (!propertyMappings.TryGetValue(jp.Name, out var name))
name = jp.Name;
PropertyInfo prop = props.FirstOrDefault(pi =>
pi.CanWrite && pi.GetCustomAttribute<JsonPropertyAttribute>().PropertyName == name);
prop?.SetValue(instance, jp.Value.ToObject(prop.PropertyType, serializer));
}
return instance;
}
public override bool CanConvert(Type objectType)
{
return objectType.GetTypeInfo().IsClass;
}
public override bool CanWrite => false;
}
... Add the JsonConverter attribute to the class...
[JsonConverter(typeof(ManaeurConverter))]
public class Result
{
[JsonProperty("MANAEUR")]
public object[][] Manaeur { get; set; }
[JsonProperty("last")]
public int Last { get; set; }
}
... and parse like so...
string json_Manaeur = "{\"error\":[],\"result\":{\"MANAEUR\":[[1619042400,\"1.11200\",\"1.13488\",\"1.08341\",\"1.10077\",\"1.09896\",\"58878.56534370\",137],[1619046000,\"1.09767\",\"1.12276\",\"1.08490\",\"1.11097\",\"1.10456\",\"25343.25910419\",77]],\"last\":1619118000}}";
string json_Not_Manaeur = "{\"error\":[],\"result\":{\"NOTMANAEUR\":[[1619042400,\"1.11200\",\"1.13488\",\"1.08341\",\"1.10077\",\"1.09896\",\"58878.56534370\",137],[1619046000,\"1.09767\",\"1.12276\",\"1.08490\",\"1.11097\",\"1.10456\",\"25343.25910419\",77]],\"last\":1619118000}}";
var objManaeur = JsonConvert.DeserializeObject<RootObject>(json_Manaeur);
var objNotManaeur = JsonConvert.DeserializeObject<RootObject>(json_Not_Manaeur);
I get this json format from an API:
"url_service": "",
"Description": null,
"Target": "5,6",
"Category ": "2"
I'm trying to deserialize the json into a model. The trouble is with the "Target" field which is supposed to be an ICollection of int.
Here is my model:
public string Description{ get; set; }
public ICollection<int> Target{ get; set; }
public int Category { get; set; }
Is there a way to process the Json before it gets serialised in order to make a collection out of the comma separated string?
Instead of trying to change the deserialization logic, why not make it easier on yourself and just include a new property in your model?
public string Description{ get; set; }
public int Category { get; set; }
//Change this back to string, since that is what your JSON is
public string Target{ get; set; }
//Add a "TargetList" property that just reads from "Target" and turns into a List
public List<int> TargetList => Target.Split(',').Select(x => int.Parse(x)).ToList();
Be aware that there is no error handling in my code so you will have to modify accordingly.
Yes, have your C# class implement the ISerializable interface. There are OnDeserialized(), OnSerialized() functions you could implement.
Refer to .NET Deserialisation with OnDeserializing and OnDeserialized
Your JSON fragment does not describe a collection of integers but a string. The correct one would be
"Target": [ 5, 6 ],
which translates into a JSON schema of
"Target": {
"type": ["array"],
"items": { "type": "integer"}
},
If you don't have control of the result then create another property which will be a result of integers such as
private string _target;
public string Target { get { return _target; }
set {
_target = value;
Targets = Regex.Matches(_target, #"(\d+)")
.OfType<Match>()
.Select(mt => int.Parse(mt.Value))
.ToList();
public List<int> Targets { get; set; }
The Target field is of string type in your json, hence the serialiser will attempt to read it as a string.
You can use a converter to overrule that, for example using Newtonsoft Json.
Let's assume your data structure to be defined as follows:
public class Data {
public string Description{ get; set; }
public ICollection<int> Target{ get; set; }
public int Category { get; set; }
}
Then you may create your own JsonConverter as follows:
public class DataConverter : JsonConverter {
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
var jObject = JObject.Load(reader);
var data = new Data();
if (jObject.TryGetValue("Description", out JToken jDescription)) {
data.Description = jDescription.Value<string>();
}
if (jObject.TryGetValue("Target", out JToken jTarget)) {
data.Target = ToTarget(jTarget, serializer);
}
if (jObject.TryGetValue("Category", out JToken jCategory)) {
data.Category = jCategory.Value<int>();
}
return req;
}
private static ICollection<int> ToTarget( JToken jTarget, JsonSerializer serializer ) {
int defaultValue = -1;
var target = new List<int>();
var collection = jTarget.Value<string>().Split(',');
foreach (string str in collection)
if (int.TryParse(str, out int number))
target.Add(number);
else
target.Add(defaultValue);
return target;
}
public override bool CanConvert(Type objectType) => objectType == typeof(Data);
}
You can then deserialise using the following code:
var jsonSettings = new JsonSerializerSettings();
jsonSettings.Converters.Add(new DataConverter);
Data data = JsonConvert.DeserializeObject<Data>(yourJsonString, jsonSettings);
Further considerations: this approach provides clear separation between your data definition and the data parsing logic, hence keeping your data structure and definition clean from any json specific information and parsing logic.
Not too familiar with JSON yet and have come across a an issue that not obvious to me.
The api I'm querying returns a standard response object with the result of the processed command/api request embedded within a data object within the json.
So the response comes back like the following for all requests on the API the data component changes dependent on whats been requested.
ObjectType1 response
{
"data": {
"person" : {
"id" : 21,
"name" : "Json can be annoying at times"
}
},
"message" : "",
"result" : "success"
}
or another request on api will return the following List of
ObjectType2 response
{
"data": {
"1234" : {
"id": 1234,
"title" : "Terminator"
},
"3245" : {
"id" : 3245,
"name" : "Terminator 2"
}
},
"message" : "",
"result" : "success"
}
I would like to have a custom JsonConverter that pulls out the response into an object as such
public class MyResponse {
[JsonProperty(PropertyName = "data")]
public string Data { get; set; }
[JsonProperty(PropertyName = "message")]
public string Message { get; set; }
[JsonProperty(PropertyName = "status")]
public string Status { get; set; }
}
or
public class MyResponse<T> : class T {
public T Data { get; set; }
public string Message { get; set; }
public string Status { get; set; }
}
And then from there i can act on the Status / message within a generic method then return a json string back to the calling method in my library. From which the json string returned can be processed properly according to the request.
Any ideas how to deserialize data's child object back into a string or even better if i passed the method a generic type T how could i deserialize the json into both objects.
EDIT
Posted answer below for those looking to do something similar
Cheers
Thank you to those that offered some help, but I eventually came up with the answer I was looking for that would deserialize my object into a proper type offered via generics.
Here is my MyCustomResponse object
public class MyCustomResponse
{
[JsonProperty(PropertyName = "data")]
public object Data { get; set; }
[JsonProperty(PropertyName = "message")]
public string Message { get; set; }
[JsonProperty(PropertyName = "result")]
public string Result { get; set; }
}
The custom JsonConverter ended up like so, I look for the property in the json string "data" then convert it to an object of type T
public class MyCustomResponseConverter<T> : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(MyCustomResponse));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
object instance = objectType.GetConstructor(Type.EmptyTypes).Invoke(null);
PropertyInfo[] props = objectType.GetProperties();
JObject jo = JObject.Load(reader);
foreach ( JProperty jp in jo.Properties() )
{
PropertyInfo prop = props.FirstOrDefault(pi =>
pi.CanWrite && string.Equals(pi.Name, jp.Name, StringComparison.OrdinalIgnoreCase));
if ( prop != null )
{
// Convert data object to what was passed in at T
if ( jp.Name == "data" )
prop.SetValue(instance, jo.SelectToken("data").ToObject(typeof(T)));
else
prop.SetValue(instance, jp.Value.ToObject(prop.PropertyType, serializer));
}
}
return instance;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
To use the above I created a Generic method that looks like the following and allows me to pass the command to run, extra query string and also the Type to convert 'Data' object to:
private async Task<T> GenericApiRequestAsync<T>(string command, string query)
{
HttpClient client = new HttpClient();
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
Uri uri = new Uri(string.Format("{0}/api/{1}/?cmd={2}{3}", apiUrl, apiKey, command, query));
try {
HttpResponseMessage response = await client.GetAsync(uri);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
// Convert responseContent via MyCustomResponseConverter
var myCustomResponse =
await Task.Factory.StartNew(() =>
JsonConvert.DeserializeObject<MyCustomResponse(
responseContent,
new MyCustomResponseConverter<T>()
));
return (T)myCustomResponse.Data;
}
catch(Exception ex)
{
...
}
}
Then to use the actual GenericApiRequestAsync method i simply pass it the command, query and type for the Data object to be converted into, whatever it maybe.
public async Task<Person> GetPersonAsync(int id)
{
return await GenericApiRequestAsync<Person>("person.byid", string.Format("&id={0}", id));
}
public async Task<IDictionary<string, ObjectType2>> GetObjectType2ListAsync(string name)
{
return await GenericApiRequestAsync<IDictionary<string, ObjectType2>>("show.byname", string.Format("&name={0}", name));
}
Ended up a simple solution but complex in getting there. It removes the need to process the data object a second time into in the final object as well.
Hope this solution can help others out there that come across similar JSON structures, if anyone sees a simpler way to implement the converter, i'm happy to accept any input.
Cheers
For serialization/deserialization of JSON objects, take a look at Json.NET. You can include it as a Nuget package and use built in methods such as JsonConvert.SerializeObject(Object object) and JsonConvert.DeserializeObject(string value, Type type).
You can control the name of the JSON properties by decorating your models with JsonProperty attributes. For example:
public class MyResponse {
[JsonProperty(PropertyName = "data")]
public string Data { get; set; }
[JsonProperty(PropertyName = "message")]
public string Message { get; set; }
[JsonProperty(PropertyName = "status")]
public string Status { get; set; }
}
Two entities:
public class Response
{
public Dictionary<string, Data> data { get; set; }
public string message { get; set; }
public string result { get; set; }
}
public class Data
{
public int id { get; set; }
public string title { get; set; }
}
The response code is:
JavaScriptSerializer serializer = new JavaScriptSerializer();
Response response = (Response) serializer.Deserialize<Response>(jsonString);
As you can see there is no additional packages