Related
I'd like to be able to control how JSON .NET generates its meta reference IDs such as "$id": "1". Take the following code:
public class Person
{
public string Name { get; set; }
public Person Mother { get; set; }
}
.
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects,
Formatting = Newtonsoft.Json.Formatting.Indented
};
Newtonsoft.Json.JsonConvert.DefaultSettings = () => settings;
var person = new Person
{
Name = "bob",
Mother = new Person { Name = "jane" }
};
var personJson = JsonConvert.SerializeObject(person);
var motherJson = JsonConvert.SerializeObject(person.Mother);
The JSON for person looks like this:
{
"$id": "1",
"Name": "bob",
"Mother": {
"$id": "2",
"Name": "jane",
"Mother": null
}
}
However, if I serialize person.Mother directly, the JSON looks like this:
{
"$id": "1",
"Name": "jane",
"Mother": null
}
In the first JSON, Jane is "$id": "2", but serializing Jane directly is "$id": "1". This is the behavior I'd expect under normal conditions as the serializer assigns the IDs in the order it traverses the objects, but I'd really like to override the ID generation so that I could make it a hash of the object reference itself. This way, Jane would generate the same ID per running instance of the program every time regardless if serialized as a member of a parent or serialized individually.
UPDATE
Per sample code in selected answer and recommendation in comment, I have used IReferenceResolver. It turns out that I can't use it, though, but I'll include the code below anyway. The reason why this won't work is because I am trying to bastardize JSON.NET as a quick and dirty cloning tool, so I can't fault it for not suiting my needs. I've since fallen back on my own custom cloning utility, so I no longer need this.
public class ObjectReferenceResolver : Newtonsoft.Json.Serialization.IReferenceResolver
{
readonly Dictionary<object, int> objectDic = new Dictionary<object, int>();
int maxId = 0;
//Called during serialization
public string GetReference(object context, object value)
{
//This method will return the meta $id that you choose. In this example, I am storing
//object references in a dictionary with an incremented ID. If the reference exists, I
//return its ID. Otherwise, I increment the ID and add the reference to the dictionary.
var id = 0;
if (objectDic.ContainsKey(value))
{
id = objectDic[value];
}
else
{
objectDic[value] = maxId++;
}
return id.ToString();
}
//Called during serialization
public bool IsReferenced(object context, object value)
{
//Return whether or not value exists in a reference bank.
//If false, the JSON will return as a full JSON object with "$id": "x"
//If true, the JSON will return "$ref": "x"
return objectDic.ContainsKey(value);
}
//Called during deserialization
public void AddReference(object context, string reference, object value)
{
//This method is called after the deserializer has created a new instance of the
//object. At this time, it's only the initial instance and no properties have been set.
//This method presents a problem because it does not allow you to create the instance or
//retrieve it from a repo and then return it for later use by the reference resolver.
//Therefore, I have to find the existing object by $id, remove it, and then add the new
//object created by the deseralizer. This creates the possibility for two instances of
//the same data object to exist within the running application, so, unfortunately, this
//will not work.
var e = objectDic.First(x => x.Value.ToString() == reference).Key;
objectDic.Remove(e);
objectDic[value] = reference.ParseInt().Value;
}
//Called during deserialization
public object ResolveReference(object context, string reference)
{
//This method retrieves an existing reference by $id and returns it.
var value = objectDic.FirstOrDefault(x => x.Value.ToString() == reference).Key;
return value;
}
}
As per others have recommended, you need a custom IReferenceResolver:
class PersonNameAsIdResolver : IReferenceResolver
{
public void AddReference(object context, string reference, object value)
{
// This method is called during deserialize for $id
}
public string GetReference(object context, object value)
{
// Returns person name as value of $id
return ((Person)value).Name;
}
public bool IsReferenced(object context, object value)
{
// Returns false, so that $id is used, not $ref.
return false;
}
public object ResolveReference(object context, string reference)
{
// This method is called during deserialize for $ref
return null;
}
}
To use that:
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects,
Formatting = Newtonsoft.Json.Formatting.Indented
};
settings.ReferenceResolverProvider = ()=> new PersonNameAsIdResolver();
UPDATE
Answer to the OP's update
AddReference is called while an object is being populated, so it has been too late to replace the object. To be able to find and populate desired object, you need a JsonConverter, which is called before the reference resolver:
class PersonJsonConverter : JsonConverter
{
private readonly PersonNameAsIdResolver _idResolver;
public PersonJsonConverter(PersonNameAsIdResolver idResolver)
{
_idResolver = idResolver;
}
public override bool CanConvert(Type objectType)
=> objectType == typeof(Person);
// Can't write. There's nothing to changing for writing scenario.
public override bool CanWrite => false;
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
var token = JToken.ReadFrom(reader);
if (token.Type == JTokenType.Null)
{
return null;
}
var obj = (JObject)token;
// The code below calls the resolver to find the existing instance.
// This can stop JSON.NET creating a new instance.
Person instance = null;
var #id = obj["$id"].Value<string>();
if (#id != null)
{
instance = (Person)_idResolver.ResolveReference(this, #id);
}
else
{
var #ref = obj["$ref"]?.Value<string>();
if (#ref != null)
{
instance = (Person)_idResolver.ResolveReference(this, #ref);
}
}
// Assuming can't resolve, create a new instance.
if (instance == null)
{
instance = new Person();
}
// This will populate existing Person object if found
serializer.Populate(obj.CreateReader(), instance);
return instance;
}
public override void WriteJson(JsonWriter writer, object value,
JsonSerializer serializer)
{
throw new NotSupportedException();
}
}
And the default serialization settings should look like:
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.Objects,
Formatting = Newtonsoft.Json.Formatting.Indented
};
var idResolver = new PersonNameAsIdResolver();
settings.Converters.Add(new PersonJsonConverter(idResolver));
settings.ReferenceResolverProvider = () => idResolver;
A possible solution would be the following:
Replace PreserveReferncesHandling from Objects to None:
PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.None
Add an Id property in Person class:
public class Person
{
public string Id { get; set; }
public string Name { get; set; }
public Person Mother { get; set; }
}
The complete solution is as follows:
using System;
using Newtonsoft.Json;
namespace ControlJsonId
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("INICIO");
var settings = new Newtonsoft.Json.JsonSerializerSettings
{
PreserveReferencesHandling = Newtonsoft.Json.PreserveReferencesHandling.None,
Formatting = Newtonsoft.Json.Formatting.Indented,
};
Newtonsoft.Json.JsonConvert.DefaultSettings = () => settings;
var person = new Person
{
Id = Guid.NewGuid().ToString(),
Name = "bob",
Mother = new Person { Id = string.Empty, Name = "jane" }
};
var personJson = JsonConvert.SerializeObject(person);
Console.WriteLine(personJson);
var motherJson = JsonConvert.SerializeObject(person.Mother);
Console.WriteLine(motherJson);
Console.WriteLine("FIN");
Console.ReadKey();
}
}
public class Person
{
public string Id { get; set; }
public string Name { get; set; }
public Person Mother { get; set; }
}
}
The result is:
INICIO
{
"Id": "76d6b5f0-2be8-4d1d-aafe-fe1b4b7d6ae1",
"Name": "bob",
"Mother": {
"Id": "",
"Name": "jane",
"Mother": null
}
}
{
"Id": "",
"Name": "jane",
"Mother": null
}
FIN
I am writing converter for json like this:
{
"datatable": {
"data": [
[
"A85002072C",
"1994-11-15",
678.9
]
],
"columns": [
{
"name": "series_id",
"type": "String"
},
{
"name": "date",
"type": "Date"
},
{
"name": "value",
"type": "double"
}
]
},
"meta": {
"next_cursor_id": null
}
}
At the moment my converter looks like this:
public class AbsToModelConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType.Name.Equals("AbsFseModel");
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JArray array = JArray.Load(reader);
return new QuandlAbsModel
{
SeriesId = array[0].ToString(),
Date = array[1].ToObject<DateTime>(),
Value = array[2].ToObject<decimal?>()
};
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var orderItem = value as QuandlAbsModel;
JArray arra = new JArray();
arra.Add(orderItem.SeriesId);
arra.Add(orderItem.Date);
arra.Add(orderItem.Value);
arra.WriteTo(writer);
}
}
It works at the moment, but when i am using filters my json can contain not full data, for example:
"data":[["1994-11-15",678.9]]
And my JsonConverter stops working, because there is no element array[2] and it throws error. Problem is that elements in data array don't have names (i get JSON from web API, so i can't change json at all). Is there any way to make my converter deserialize json with filters?
I have column names in my json after the data table, maybe this will help. But i don't understand how i can use them atm. Any advices?
You don't need a JsonConverter for this.
Define classes to represent the parts of the JSON you need:
class APIResponse
{
public DataTable DataTable { get; set; }
}
class DataTable
{
public object[][] Data { get; set; }
}
Use JsonConvert.DeserializeObject<T>() to deserialize the JSON:
var parsed = JsonConvert.DeserializeObject<APIResponse>(json);
Then get your values:
var rows = parsed.DataTable.Data.Select(r => new QuandLabsModel
{
SeriesId = Convert.ToString(r[0]),
Date = Convert.ToDateTime(r[1]),
Value = Convert.ToDecimal(r[2])
});
JLRishe is correct that your problem is solvable without a custom converter. That's a good approach in many cases. If you're able to insert a translation over the JSON serializer/deserializer, it might be simpler to write, understand, and maintain than a custom JsonConverter. It's similar in spirit to the "serialization proxy pattern" used in the Java world. In essence, you're copying your data to a new serialization-specific object before serializing, and then doing the reverse to re-serialize.
This problem is solvable with a custom converter, and I've written an example to show that it can be done, but do consider using a translation proxy/layer first.
This example is a proof-of-concept; not production-ready code. I made very little effort to defend against malformed input or other errors. Its handling of the different fields/types is also very rudimentary--any changes to the fields/types will require changes to the converter. That sort of brittleness is likely to cause bugs and maintenance headaches over time.
To narrow down the problem a bit, I reduced the original question's sample JSON to its bare minimum:
{
"datatable": {
"data": [
"A85002072C",
"1994-11-15",
678.9
],
"columns": [
{
"name": "series_id"
},
{
"name": "date"
},
{
"name": "value"
}
]
}
}
For reference, here's the C# class definition I'm deserializing to:
public class Model
{
public string SeriesId { get; set; }
public DateTime Date { get; set; }
public Decimal? Value { get; set; }
}
And here's the proof-of-concept converter:
public sealed class ModelConverter : JsonConverter
{
public static readonly ModelConverter Instance = new ModelConverter();
private ModelConverter() {}
public override bool CanConvert(Type objectType) => objectType == typeof(Model);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var obj = JObject.Load(reader);
var data = (JArray)obj["datatable"]["data"];
var columns = (JArray)obj["datatable"]["columns"];
if (data.Count != columns.Count)
throw new InvalidOperationException("data and columns must contain same number of elements");
var model = new Model();
for (int i = 0; i < data.Count; i++)
{
// A "switch" works well enough so long as the number of fields is finite and small.
// There are smarter approaches, but I've kept the implementation basic
// in order to focus on the core problem that was presented.
switch (columns[i]["name"].ToString())
{
case "series_id":
model.SeriesId = data[i].ToString();
break;
case "date":
model.Date = data[i].ToObject<DateTime>();
break;
case "value":
model.Value = data[i].ToObject<decimal?>();
break;
}
}
return model;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var data = new JArray();
var columns = new JArray();
var model = (Model)value;
// Like the "switch" used in deserialization, these "if" blocks are
// pretty rudimentary. There are better ways, but I wanted to keep
// this proof-of-concept implementation simple.
if (model.SeriesId != default(string))
{
data.Add(model.SeriesId);
columns.Add(new JObject(new JProperty("name", "series_id")));
}
if (model.Date != default(DateTime))
{
data.Add(model.Date.ToString("yyyy-MM-dd"));
columns.Add(new JObject(new JProperty("name", "date")));
}
if (model.Value != default(Decimal?))
{
data.Add(model.Value);
columns.Add(new JObject(new JProperty("name", "value")));
}
var completeObj = new JObject();
completeObj["datatable"] = new JObject();
completeObj["datatable"]["data"] = data;
completeObj["datatable"]["columns"] = columns;
completeObj.WriteTo(writer);
}
}
I wrote a few unit tests to verify the serializer. The tests are based on xUnit.Net:
[Fact]
public void TestDeserializeSampleInputWithAllFields()
{
var json = File.ReadAllText(BasePath + "sampleinput.json");
var obj = JsonConvert.DeserializeObject<Model>(json, ModelConverter.Instance);
Assert.Equal("A85002072C", obj.SeriesId);
Assert.Equal(new DateTime(1994, 11, 15), obj.Date);
Assert.Equal(678.9M, obj.Value);
}
[Fact]
public void TestSerializeSampleInputWithAllFields()
{
var model = new Model
{
SeriesId = "A85002072C",
Date = new DateTime(1994, 11, 15),
Value = 678.9M,
};
var expectedJson = File.ReadAllText(BasePath + "sampleinput.json");
Assert.Equal(expectedJson, JsonConvert.SerializeObject(model, Formatting.Indented, ModelConverter.Instance));
}
And to prove that the serializer works without all fields present:
{
"datatable": {
"data": [
"B72008039G",
543.2
],
"columns": [
{
"name": "series_id"
},
{
"name": "value"
}
]
}
}
[Fact]
public void TestDeserializeSampleInputWithNoDate()
{
var json = File.ReadAllText(BasePath + "sampleinput_NoDate.json");
var obj = JsonConvert.DeserializeObject<Model>(json, ModelConverter.Instance);
Assert.Equal("B72008039G", obj.SeriesId);
Assert.Equal(default(DateTime), obj.Date);
Assert.Equal(543.2M, obj.Value);
}
[Fact]
public void TestSerializeSampleInputWithNoDate()
{
var model = new Model
{
SeriesId = "B72008039G",
Value = 543.2M,
};
var expectedJson = File.ReadAllText(BasePath + "sampleinput_NoDate.json");
Assert.Equal(expectedJson, JsonConvert.SerializeObject(model, Formatting.Indented, ModelConverter.Instance));
}
I'm getting an irregular JSON array from the Census Bureau's public api.
The variable names are all in the first element, and I'm having trouble deserializing it.
http://api.census.gov/data/2014/pep/agesex?get=AGE,POP,SEX&for=us:*&DATE=7
gives me JSON like this:
[["AGE","POP","SEX","DATE","us"],
["0","3948350","0","7","1"],
["1","3962123","0","7","1"],
["2","3957772","0","7","1"],
["3","4005190","0","7","1"],
["4","4003448","0","7","1"],
["5","4004858","0","7","1"],
["6","4134352","0","7","1"],
["7","4154000","0","7","1"]]
I can successfully deserialize this using:
var test1 = JsonConvert.DeserializeObject<String[][]>(jsonStr);
However, I'm trying to deserialize it to a class like this:
public class TestClass
{
public string AGE { get; set; }
public string POP { get; set; }
public string SEX { get; set; }
public string DATE { get; set; }
public string us { get; set; }
}
I'm trying to do this:
var test2 = JsonConvert.DeserializeObject<TestClass[]>(jsonStr);
But I'm getting the following exception:
An exception of type 'Newtonsoft.Json.JsonSerializationException'
occurred in Newtonsoft.Json.dll but was not handled in user code
Additional information: Cannot create and populate list type
TestClass. Path '[0]', line 1, position
2.
There's two parts to this.
First is turning the JSON in to data usable in C#, and the second is turning that data in to nice objects.
Here's a working dotNetFiddle.net example of the following code: https://dotnetfiddle.net/Cr0aRL
Each row in your JSON is made up of an array of strings.
So that's an array of an array of strings.
In C# that can be written as string[][].
So to turn the JSON in to usable data with JSON.Net you can do:
var json = "[[\"AGE\",\"POP\",\"SEX\",\"DATE\",\"us\"],[\"0\",\"3948350\",\"0\",\"7\",\"1\"],[\"1\",\"3962123\",\"0\",\"7\",\"1\"],[\"2\",\"3957772\",\"0\",\"7\",\"1\"],[\"3\",\"4005190\",\"0\",\"7\",\"1\"],[\"4\",\"4003448\",\"0\",\"7\",\"1\"],[\"5\",\"4004858\",\"0\",\"7\",\"1\"],[\"6\",\"4134352\",\"0\",\"7\",\"1\"],[\"7\",\"4154000\",\"0\",\"7\",\"1\"]]";
var rawData = JsonConvert.DeserializeObject<string[][]>(json);
Next up is is turning that data in to objects.
The first row is the header, containing the column names, so we want to grab that, and then figure out the column index for each column name.
var headerRow = rawData.First();
var ageIndex = Array.IndexOf(headerRow, "AGE");
var popIndex = Array.IndexOf(headerRow, "POP");
var sexIndex = Array.IndexOf(headerRow, "SEX");
var dateIndex = Array.IndexOf(headerRow, "DATE");
var usIndex = Array.IndexOf(headerRow, "us");
Now we have the indexes, we need to take each row, and convert it in to the appropriate object. I've used LINQ for this as it's very good at representing data processing in a clear way.
var testData = rawData
.Skip(1) //The first row is a header, not data
.Select(dataRow => new TestClass()
{
AGE = dataRow[ageIndex],
POP = dataRow[popIndex],
SEX = dataRow[sexIndex],
DATE = dataRow[dateIndex],
us = dataRow[usIndex]
});
Finally a bit of testing, to make sure you have the data you're expecting.
//Get the second data row as an example
var example = testData.Skip(1).First();
//Output example POP to check value
Console.WriteLine(example.POP);
Everything above is very manual.
You have to know what headers you expect, then you manually find the indexes, then you manually map the rows to objects.
It's quite possible for a simple use case that doing that is fine. But in larger and/or more complex systems you might want/need to automate those steps.
Automating those steps is possible, but is beyond the scope of this answer as how you approach it can depend on a lot of different factors.
You could make a custom JsonConverter to handle this conversion during deserialization. The conversion code is really not much different than other answers here, except that it is encapsulated into a separate class so that you don't muddy up your main code with the conversion details. From the point of view of your main code it "just works".
Here is how to write the converter:
public class TestClassArrayConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(TestClass[]));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JArray table = JArray.Load(reader);
TestClass[] items = new TestClass[table.Count - 1];
for (int i = 1; i < table.Count; i++)
{
JArray row = (JArray)table[i];
items[i - 1] = new TestClass
{
AGE = (string)row[0],
POP = (string)row[1],
SEX = (string)row[2],
DATE = (string)row[3],
us = (string)row[4]
};
}
return items;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
And here is how you would use it:
var test2 = JsonConvert.DeserializeObject<TestClass[]>(jsonStr, new TestClassArrayConverter());
Fiddle: https://dotnetfiddle.net/68Q0KT
You have to do the processing on your own, as there is no way the json deserializer can know, how to put the values into the respecitve variables.
If you know, this will be exactly this structure, you could for instance add an appropriate constructor
public TestClass(string[] values) {
AGE = values[0];
...
}
to your class. Then serialize your result to array of arrays of string and then pass the inner arrays to your constructor.
var t1 = JsonConvert.DeserializeObject<string[][]>(jsonStr);
//skip the first entry, as this contains the headers
var t2 = t1.Skip(1).Select(x=> new TestClass(x));
If your structure varies, you'll have to write some more complicated mapping code.
You will have to do some custom mapping as your Json does not have any naming conventions so you will have to work with the data in array and index formats. This will work:
var jsonStr = "[[\"AGE\",\"POP\",\"SEX\",\"DATE\",\"us\"], [\"0\",\"3948350\",\"0\",\"7\",\"1\"], [\"1\",\"3962123\",\"0\",\"7\",\"1\"], [\"2\",\"3957772\",\"0\",\"7\",\"1\"], [\"3\",\"4005190\",\"0\",\"7\",\"1\"], [\"4\",\"4003448\",\"0\",\"7\",\"1\"], [\"5\",\"4004858\",\"0\",\"7\",\"1\"], [\"6\",\"4134352\",\"0\",\"7\",\"1\"], [\"7\",\"4154000\",\"0\",\"7\",\"1\"]]";
var test2 = JsonConvert.DeserializeObject<string[][]>(jsonStr);
var test3 = test2.Select(x => new TestClass()
{
AGE = x[0].ToString(),
POP = x[1].ToString(),
SEX = x[2].ToString(),
DATE = x[3].ToString(),
us = x[4].ToString()
}).ToList();
//test Case
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
namespace ApiController.Test
{
[TestClass]
public class DownloadIrregularJsonStringObjects
{
string ApiKey => "YourPersonalCensusKey";
/// <summary>
/// You have to get your own ApiKey from the Census Website
/// </summary>
[TestMethod]
public void TestGetItem()
{
string url = $"http://api.census.gov/data/timeseries/healthins/sahie?get=NIC_PT,NAME,NUI_PT&for=county:*&in=state:*&time=2015&key={YourPersonalCensusKey}";
string expected = "Autauga County, AL";
IList<HealthData> actual = ApiController.DownloadIrregularJsonStringObjects.GetCensusHealthData(url);
Assert.AreEqual(actual[0].NAME, expected);
}
}
}
///Actual Assembly
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
namespace ApiController
{
public class DownloadIrregularJsonStringObjects
{
public static IList<HealthData> GetCensusHealthData(string url)
{
var json = GetData(url);
var rawData = JsonConvert.DeserializeObject<string[][]>(json);
var headerRow = rawData.First();
var nic_pt_Index = Array.IndexOf(headerRow, "NIC_PT");
var name_Index = Array.IndexOf(headerRow, "NAME");
var nui_pt_Index = Array.IndexOf(headerRow, "NUI_PT");
IList<HealthData> retVal = new List<HealthData>();
foreach (var r in rawData.Skip(1))
{
HealthData dataRow = new HealthData();
dataRow.NIC_PT = r[nic_pt_Index];
dataRow.NAME = r[name_Index];
dataRow.NUI_PT = r[nui_pt_Index];
retVal.Add(dataRow);
}
return retVal;
}
private static string GetData(string url)
{
using (var w = new WebClient())
{
var jsonData = string.Empty;
jsonData = w.DownloadString(url);
return jsonData;
}
}
}
public class HealthData
{
public string NIC_PT { get; set; }
public string NAME { get; set; }
public string NUI_PT { get; set; }
}
}
I've written a csharp app which queries our load balancers (which are setup in an active/passive configuration) to determine which is the active. The load balancers have a REST API which I'm querying as such:
public void GetActiveLB()
{
// Create a new RestClient and RestRequest
var client = new RestClient("https://myloadbalancer.domain.local");
var request = new RestRequest("mgmt/tm/cm/failover-status", Method.GET);
// Specify authentication
client.Authenticator = new HttpBasicAuthenticator("myusername", "supersecret");
// ask for the response to be in JSON syntax
request.RequestFormat = DataFormat.Json;
//send the request to the web service and store the response when it comes back
var queryResult = client.Execute(request);
// Create a new Deserializer to be able to parse the JSON object
RestSharp.Deserializers.JsonDeserializer deserial = new JsonDeserializer();
var JSONObj = deserial.Deserialize<Dictionary<string, string>>(queryResult);
string lbstatus = JSONObj["description"];
}
The JSON returned to me looks like this:
{
"kind":"tm:cm:failover-status:failover-statusstats",
"selfLink":"https://localhost/mgmt/tm/cm/failover-status?ver=11.6.0",
"entries": {
"https://localhost/mgmt/tm/cm/failover-status/0": {
"nestedStats": {
"entries": {
"color": {
"description": "green"
},
"https://localhost/mgmt/tm/cm/failoverStatus/0/details": {
"nestedStats": {
"entries": {
"https://localhost/mgmt/tm/cm/failoverStatus/0/details/0": {
"nestedStats": {
"entries": {
"details": {
"description": "active for /Common/traffic-group-1"
}
}
}
}
}
}
},
"status": {
"description": "ACTIVE"
},
"summary": {
"description": "1/1 active"
}
}
}
}
}}
Here's a prettier formatted version:)
The path to the item I want is:
[JSON].entries.https://localhost/mgmt/tm/cm/failover-status/0.nestedStats.entries.status.description
What I am struggling with is how to get the value of this item particularly because it seems to be nested multiple times. Is there a way to provide an absolute path?
Thank You
Brad
If the json is always structured the same way you can create some poco's that nest themselves and then deserialise with the json deserialisation:
eg:
[DataContract]
public class JsonResponse
{
[datamember]
public string kind;
[datamember]
public string selflink;
[datamember]
public Entry entries; //we nest another object here just as in the json
}
[DataContract]
public class Entry
{
[datamember]
public nestedstat nestedstats;
}
(this is just loosly typed)
then:
JsonResponse response = new JavaScriptSerializer().Deserialize<JsonResponse>(jsonasstring);
The easiest way to do it is to use the dynamic keyword like this:
dynamic JSONObj =
deserial
.Deserialize<Dictionary<string, object>>(queryResult); //Notice how I am using Dictionary<string, object> instead of Dictionary<string, string> since some of the values are actually parents of other values so they will be presented as dictionaries themselves.
var lbstatus =
JSONObj
["entries"]
["https://localhost/mgmt/tm/cm/failover-status/0"]
["nestedStats"]
["entries"]
["status"]
["description"];
If you don't use the dynamic keyword then you would have to cast JSONObj["entries"] into a Dictionary<string,object>, and then when you access ["https://localhost/mgmt/tm/cm/failover-status/0"] on that dictionary, you would need to cast the result again into Dictionary<string,object> ... etc.
In this case, the dynamic keyword will make it very much easier.
If you want to get the result from an absolute path, you can do something like this with dynamic:
dynamic JSONObj = deserial.Deserialize<Dictionary<string, object>>(queryResult);
string path =
"entries.https://localhost/mgmt/tm/cm/failover-status/0.nestedStats.entries.status.description";
dynamic value = JSONObj;
foreach (var sub_path in path.Split('.'))
{
value = value[sub_path];
}
var lbstatus = value.ToString();
I´m parsing a JSON string to a corresponding .NET Object with the Newtonsoft library. I have a problem parsing JSON properties that are arrays. Sometimes the JSON property is an array, other times, it is a single element.
Example:
This is the .NET object:
public class xx
{
public string yy { get; set; }
public List<string> mm{ get; set; }
}
When i receive this JSON:
{ "xx": {"yy":"nn", "mm": [ "zzz", "aaa" ] } }
I perfectly can do:
JsonConvert.DeserializeObject<xx>(json);
But sometimes I receive this JSON:
{ "xx": {"yy":"nn", "mm":"zzz"} }
And the deserialization fails because of the list property on the C# object.
How can I define an object for deserialize the two JSON string in the same object (with List<string>).
-------- UPDATE -----
First of all a WS generate a XML doing some operation.. the XML is like
<xx yy='nn'><mm>zzz</mm></xx>
and if there are more elements is:
<xx yy='nn'><mm>zzz</mm><mm>aaa</mm></xx>
finally the WS convert this XML doing:
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml);
var json = JsonConvert.SerializeXmlNode(doc);
and send to me the json.. and here begins my problem..
Updated Answer:
Looking at how JSON.Net maps XML, it takes the approach that what it sees is what it serializes, except that if it sees multiples, it will make an array. This is great for many XML DOM trees with consistent layout, but unfortunately cannot work for your purposes.
You can verify this by looking at the body for the functions SerializeGroupedNodes() and SerializeNode() in the following file source.
XmlNodeConverter.cs source code # CodePlex, ChangeSet #63616
There's another option that I'd previously thought might be overkill but would be helpful now that we know what to expect from the default behavior on the serializing end.
Json.Net supports using custom converters derived from JsonConverter to map particular cases of values on a case-by-case basis.
We can handle this either at the serializing side or the deserializing side. I've chosen to write a solution on the deserializing side, as you probably have other existing reasons to map XML to JSON.
One great benefit is that your class can stay intact except for the override, which requires that you apply an attribute. Here's a code sample demonstrating how to use JsonAttribute and a custom converter class (MMArrayConverter) to fix your problem. Note that you will probably want to test this more thoroughly and maybe update the converter to handle other cases, say if you eventually migrate to IList<string> or some other funky case like Lazy<List<string>>, or even make it work with generics.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Converters;
namespace JsonArrayImplictConvertTest
{
public class MMArrayConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType.Equals(typeof(List<string>));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.StartArray)
{
List<string> parseList = new List<string>();
do
{
if (reader.Read())
{
if (reader.TokenType == JsonToken.String)
{
parseList.Add((string)reader.Value);
}
else
{
if (reader.TokenType == JsonToken.Null)
{
parseList.Add(null);
}
else
{
if (reader.TokenType != JsonToken.EndArray)
{
throw new ArgumentException(string.Format("Expected String/Null, Found JSON Token Type {0} instead", reader.TokenType.ToString()));
}
}
}
}
else
{
throw new InvalidOperationException("Broken JSON Input Detected");
}
}
while (reader.TokenType != JsonToken.EndArray);
return parseList;
}
if (reader.TokenType == JsonToken.Null)
{
// TODO: You need to decide here if we want to return an empty list, or null.
return null;
}
if (reader.TokenType == JsonToken.String)
{
List<string> singleList = new List<string>();
singleList.Add((string)reader.Value);
return singleList;
}
throw new InvalidOperationException("Unhandled case for MMArrayConverter. Check to see if this converter has been applied to the wrong serialization type.");
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// Not implemented for brevity, but you could add this if needed.
throw new NotImplementedException();
}
}
public class ModifiedXX
{
public string yy { get; set; }
[JsonConverter(typeof(MMArrayConverter))]
public List<string> mm { get; set; }
public void Display()
{
Console.WriteLine("yy is {0}", this.yy);
if (null == mm)
{
Console.WriteLine("mm is null");
}
else
{
Console.WriteLine("mm contains these items:");
mm.ForEach((item) => { Console.WriteLine(" {0}", item); });
}
}
}
class Program
{
static void Main(string[] args)
{
string jsonTest1 = "{\"yy\":\"nn\", \"mm\": [ \"zzz\", \"aaa\" ] }";
ModifiedXX obj1 = JsonConvert.DeserializeObject<ModifiedXX>(jsonTest1);
obj1.Display();
string jsonTest2 = "{\"yy\":\"nn\", \"mm\": \"zzz\" }";
ModifiedXX obj2 = JsonConvert.DeserializeObject<ModifiedXX>(jsonTest2);
obj2.Display();
// This test is now required in case we messed up the parser state in our converter.
string jsonTest3 = "[{\"yy\":\"nn\", \"mm\": [ \"zzz\", \"aaa\" ] },{\"yy\":\"nn\", \"mm\": \"zzz\" }]";
List<ModifiedXX> obj3 = JsonConvert.DeserializeObject<List<ModifiedXX>>(jsonTest3);
obj3.ForEach((obj) => { obj.Display(); });
Console.ReadKey();
}
}
}
Original Answer:
It would be best to fix the JSON you're receiving at the source, as many have already pointed out. You may wish to post an update showing how the XML in your updated comment is being mapped to JSON, as that would be the best route overall.
However, if you find that this is not possible and you want some way to serialize and handle the variant value after-the-fact, you can patch things up by declaring mm to be type object, and then handling the possible cases yourself using JSON.Net's Linq support. In the two scenarios you described, you'll find that declaring mm to be type object will result in either a null, a string, or a JArray being assigned to mm by the call to DeserializeObject<>.
Here's a code sample that shows this in action. There's also a case in other circumstances where you could receive a JObject, which is also covered in this sample. Note that the member function mmAsList() does the work of patching up the difference for you. Also note that I've handled null here by returning a null for List<string>; you will probably want to revise this for your implementation.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace JsonArrayUnionTest
{
public class ModifiedXX
{
public string yy { get; set; }
public object mm { get; set; }
public List<string> mmAsList()
{
if (null == mm) { return null; }
if (mm is JArray)
{
JArray mmArray = (JArray)mm;
return mmArray.Values<string>().ToList();
}
if (mm is JObject)
{
JObject mmObj = (JObject)mm;
if (mmObj.Type == JTokenType.String)
{
return MakeList(mmObj.Value<string>());
}
}
if (mm is string)
{
return MakeList((string)mm);
}
throw new ArgumentOutOfRangeException("unhandled case for serialized value for mm (cannot be converted to List<string>)");
}
protected List<string> MakeList(string src)
{
List<string> newList = new List<string>();
newList.Add(src);
return newList;
}
public void Display()
{
Console.WriteLine("yy is {0}", this.yy);
List<string> mmItems = mmAsList();
if (null == mmItems)
{
Console.WriteLine("mm is null");
}
else
{
Console.WriteLine("mm contains these items:");
mmItems.ForEach((item) => { Console.WriteLine(" {0}", item); });
}
}
}
class Program
{
static void Main(string[] args)
{
string jsonTest1 = "{\"yy\":\"nn\", \"mm\": [ \"zzz\", \"aaa\" ] }";
ModifiedXX obj1 = JsonConvert.DeserializeObject<ModifiedXX>(jsonTest1);
obj1.Display();
string jsonTest2 = "{\"yy\":\"nn\", \"mm\": \"zzz\" }";
ModifiedXX obj2 = JsonConvert.DeserializeObject<ModifiedXX>(jsonTest2);
obj2.Display();
Console.ReadKey();
}
}
}
What the sending service sends is supposed to conform to a contract. If it doesn't, then well, either you beat up the sending developer and make them fix it, or the various things that are sent to you are the contract. A pity you don't have any metadata to know for sure, you'll just have to try a variety of contracts until one works.
object someValue;
try
{
someValue =JsonConvert.DeserializeObject<TypeWithList>(json);
}
catch
{
try
{
someValue = JsonConvert.DeserializeObject<TypeWithString>(json);
}
catch
{
//Darn, yet another type
}
}
In your case you can directly use the static method from JsonConvert class
PopulateObject(string value, object target, JsonSerializerSettings settings);
pass the JsonSerializerSettings object as
new JsonSerializerSettings(){TypeNameHandling = TypeNameHandling.All})
I think you need to look at your Javascript object. If you explicitly declare the type of the properties of the object that you are going to serialize into JSON you shouldn't run into any inconsistencies.
var stringProperty = new String();
var arrayProperty = new Array();
// Assign value to stringProperty and push elements into arrayProperty
var object = {
stringProperty: stringProperty,
arrayProperty: arrayProperty
};
var jsonObject = JSON.stringify(object);
document.write(jsonObject);
If you look at the output you will see that arrayProperty is always serialized into an array whether there are zero, one or many elements.