C# - Json deserialize object with child attributes - c#

I have the following json:
{
"issue" :
{
"id": 1,
"project":
{
"id":1,
"name":"name of project"
}
}
}
I'm trying to deserialize this json to the follwing class:
public class Issue
{
public int? id { get; set; }
public int project_id { get; set; }
public string project_name { get; set; }
}
Theres a way to get the child attribute and set to the father?

One of the simplest solution is to convert to JObject and using that creates the the required object from it.
var jObject = JsonConvert.DeserializeObject<JObject>(text);
var issue = new Issue() {id = (int?)jObject["issue"]["id"], project_id = (int)jObject["issue"]["project"]["id"], project_name = (string)jObject["issue"]["project"]["name"]};
Below code does the mentioned:
using System;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
public class Issue
{
public int? id { get; set; }
public int project_id { get; set; }
public string project_name { get; set; }
public override string ToString()
{
return "Id: " + id + " project Id: " + project_id + " project name : " + project_name;
}
}
public class Program
{
public static void Main()
{
var text = "{ \"issue\" : { \"id\": 1, \"project\": { \"id\": 2, \"name\":\"name of project\" }}}";
var jObject = JsonConvert.DeserializeObject<JObject>(text);
var issue = new Issue() {id = (int?)jObject["issue"]["id"], project_id = (int)jObject["issue"]["project"]["id"], project_name = (string)jObject["issue"]["project"]["name"]};
Console.WriteLine(issue);
}
}
You can check the Live demo here.

You need to create new Class for project :
Issue Class :
public class Issue
{
public int id { get; set; }
public Project project { get; set; }
}
Project Class :
public class Project
{
public int id { get; set; }
public String name { get; set; }
}
If you really need to have project_id and project_name in your Issue Class, you can do this :
public class Issue
{
public int id { get; set; }
public Project project { get; set; }
public int getProjectId() {
return this.getProject.getId;
}
//Do the same for projectName
}
Hope this help.

Here is a way to do that
And here is the code
public class ConventionBasedConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(YOUR-OBJECT).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var daat = JObject.Load(reader);
var yourObject = new YOUR-OBJECT();
foreach (var prop in yourObject GetType().GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance))
{
var attr = prop.GetCustomAttributes(false).FirstOrDefault();
if (attr != null)
{
var propName = ((JsonPropertyAttribute)attr).PropertyName;
if (!string.IsNullOrWhiteSpace(propName))
{
//split by the delimiter, and traverse recursevly according to the path
var conventions = propName.Split('/');
object propValue = null;
JToken token = null;
for (var i = 0; i < conventions.Length; i++)
{
if (token == null)
{
token = daat[conventions[i]];
}
else {
token = token[conventions[i]];
}
if (token == null)
{
//silent fail: exit the loop if the specified path was not found
break;
}
else
{
//store the current value
if (token is JValue)
{
propValue = ((JValue)token).Value;
}
}
}
if (propValue != null)
{
//workaround for numeric values being automatically created as Int64 (long) objects.
if (propValue is long && prop.PropertyType == typeof(Int32))
{
prop.SetValue(yourObject, Convert.ToInt32(propValue));
}
else
{
prop.SetValue(yourObject, propValue);
}
}
}
}
}
return yourObject;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
}
}
Then use it like:
var settings = new JsonSerializerSettings();
settings.Converters.Add(new ConventionBasedConverter());
JsonConvert.DeserializeObject<YOUR-OBJECT>(jsonString, settings);

Here is another way to do it. With Cinchoo ETL - an open source library along with JSON path, you can do the deserialization with few lines of code
public class Issue
{
[ChoJSONRecordField(JSONPath = "$..id")]
public int? id { get; set; }
[ChoJSONRecordField(JSONPath = "$..project.id")]
public int project_id { get; set; }
[ChoJSONRecordField(JSONPath = "$..project.name")]
public string project_name { get; set; }
}
static void Sample33()
{
string json = #"{
""issue"" :
{
""id"": 1,
""project"":
{
""id"":1,
""name"":""name of project""
}
}
}";
var issue = ChoJSONReader<Issue>.LoadText(json).First();
}
Disclaimer: I'm the author of this library.

Related

How to ingest JSON independent of top level node names

I have an Azure Server-less Function that serves to take in a JSON payload and work on the records contained. The function works perfectly well to do what is intended except it shouldn't matter the wrapper node name. For example:
{
"Wrapper": [{
"Field1": "Apple",
"Field2": "Peach",
"Field3": "########5",
"Field4": "Kiwi",
}]
}
Should be processed the same way as:
{
"OtherWrapperName": [{
"Column1": "Apple",
"Something": "Peach",
"SomethingElse": "Banana",
"Field4": "Kiwi"
}]
}
Right now it seems to expect the top level node to be called "Wrapper". Here is my attempt at this (some code has been redacted as it was unnecessary for this example):
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
string InputData = await req.Content.ReadAsStringAsync();
var inputData = JsonConvert.DeserializeObject<ItemsPayload>(InputData);
var propertiesLookup = new Dictionary<string, ItemUpdate>();
var propertiesRequest = new PropertySearchRequest { Registry = new List<RequestPropertySearch>() };
int recordCounter = 0;
foreach (var item in inputData.Wrapper)
{
foreach (var kvp in item.Where(property => property.Value.StartsWith("#!!!#")))
{
propertiesLookup[recordCounter.ToString() + "|" + kvp.Value] = new ItemUpdate
{
Properties = item,
UpdateKey = kvp.Key
};
propertiesRequest.Registry.Add(new RequestPropertySearch
{
Token = kvp.Value
});
recordCounter++;
}
}
var intermediateRequest = JsonConvert.SerializeObject(propertiesRequest, Formatting.Indented);
HttpResponseMessage response = MakeRequest(serviceUrl, intermediateRequest, securityHeaderName, securityHeaderValue);
var responseBodyAsText = response.Content.ReadAsStringAsync();
var intermediateData = JsonConvert.DeserializeObject<PropertySearchResponse>(responseBodyAsText.Result);
recordCounter = 0;
foreach (var item in intermediateData.Registry)
{
if (item.Value != null)
{
var itemToUpdate = propertiesLookup[recordCounter.ToString() + "|" + item.Token];
itemToUpdate.Properties[itemToUpdate.UpdateKey] = item.Value;
if (directive.ToLower() == "s")
{
itemToUpdate.Properties[$"#{itemToUpdate.UpdateKey}"] = item.Token;
}
// recordCounter++;
}
recordCounter++;
}
var result = JsonConvert.SerializeObject(inputData, Formatting.Indented);
//return req.CreateResponse(HttpStatusCode.OK, "");
return new HttpResponseMessage()
{
Content = new StringContent(result, System.Text.Encoding.UTF8, "application/json")
};
}
Models:
public class ItemsPayload
{
//public string Directive { get; set; }
public List<Dictionary<string, string>> Wrapper { get; set; }
}
public class PropertySearchRequest
{
public List<RequestPropertySearch> Registry { get; set; }
}
public class RequestPropertySearch
{
public string Token { get; set; }
}
public class PropertySearchResponse
{
public List<ResponsePropertySearch> Registry { get; set; }
}
public class ResponsePropertySearch
{
public string Token { get; set; }
public string Value { get; set; }
public string ProcessId { get; set; }
public string Code { get; set; }
public string Remote { get; set; }
public string Message { get; set; }
}
public class ItemUpdate
{
public Dictionary<string, string> Properties { get; set; }
public string UpdateKey { get; set; }
}
I think the ItemsPayload class property "Wrapper" is causing this as if you change that to something else and rename the node in the JSON it works fine, but I want it to be independent of the name of the top level node. Any thoughts?
You could create a simple JsonConverter for your ItemsPayload to handle the varying wrapper name.
public class ItemsPayloadConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(ItemsPayload);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject obj = JObject.Load(reader);
ItemsPayload payload = new ItemsPayload();
// Get the first property of the outer JSON object regardless of its name
// and populate the payload from it
JProperty wrapper = obj.Properties().FirstOrDefault();
if (wrapper != null)
{
payload.Wrapper = wrapper.Value.ToObject<List<Dictionary<string, string>>>(serializer);
}
return payload;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Then, just annotate your ItemsPayload class with a [JsonConverter] attribute like this and it should work with no other changes to your code:
[JsonConverter(typeof(ItemsPayloadConverter))]
public class ItemsPayload
{
public List<Dictionary<string, string>> Wrapper { get; set; }
}
Fiddle: https://dotnetfiddle.net/9q4tgW

Deserializing BSON ReadBsonType can only be called when State is Type

I have the following Code:
using MongoDB.Bson;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver;
using MongoDBTest;
using ServiceStack;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace protocol.server.API.Clients
{
public class ClientService : ServiceStack.Service
{
class CylinderSerializer : SerializerBase<Cylinder>
{
public override void Serialize(MongoDB.Bson.Serialization.BsonSerializationContext context, MongoDB.Bson.Serialization.BsonSerializationArgs args, Cylinder value)
{
var wr = context.Writer;
wr.WriteStartDocument();
wr.WriteName("_id");
wr.WriteObjectId(ObjectId.GenerateNewId());
wr.WriteName("description");
wr.WriteString(value.description.type);
context.Writer.WriteEndDocument();
}
public override Cylinder Deserialize(MongoDB.Bson.Serialization.BsonDeserializationContext context, MongoDB.Bson.Serialization.BsonDeserializationArgs args)
{
context.Reader.ReadStartDocument();
Cylinder a = new Cylinder();
a.Id = context.Reader.ReadObjectId();
while (context.Reader.State != BsonReaderState.Type && context.Reader.ReadBsonType() != BsonType.EndOfDocument)
{
a.description.type = context.Reader.ReadString();
a.description.kind = context.Reader.ReadString();
a.description.year = (short)context.Reader.ReadInt32();
a.description.producer = context.Reader.ReadString();
}
return a;
}
public async Task<List<Cylinder>> Get(GetObjects request)
{
MongoDB.Bson.Serialization.BsonSerializer.RegisterSerializer(typeof(Cylinder), new CylinderSerializer());
IMongoCollection<Cylinder> collection = Connect._database.GetCollection<Cylinder>("Cylinders");
var results = await collection.Find(_ => true).ToListAsync();
return results;
}
}
}
and get the error:
ReadBsonType can only be called when State is Type, not when State is Value
in line:
while (context.Reader.ReadBsonType() != BsonType.EndOfDocument)
I want to deserialize my objects, they look like this:
{
"_id" : ObjectId("5826010eb831ee1c70df5f16"),
"description" : {
"type" : "Cylinder",
"kind" : "rgdgg",
"year" : NumberInt(1997),
"producer" : "hnnghng",
"brands" : [
"trhr"
],
"model" : [
"Baws"
],
"internalproducerdesignation" : "tw6",
"origin" : "Greece"
},
"elements" : {
"nonspringelements" : NumberInt(0),
"springelements" : NumberInt(11),
"discelements" : NumberInt(0),
"magneticelements" : NumberInt(0),
"activeelements" : NumberInt(11),
"passiveelements" : NumberInt(0),
"totalelements" : NumberInt(11)
},
"profiles" : [
"d1",
"d11"
],
"certifications" : [
"",
""
],
"colors" : [
"brown",
"chrome"
],
"specialfittings" : [
"gf",
"hrthr",
"hgnn",
"ngnn",
"hngngn",
"nghnnn"
],
"cutdepths" : NumberInt(7),
"rareness" : "rare",
"value" : {
"new" : "0",
"used" : "$50"
},
"Blaw" : {
"tgtgt" : 10.0,
"hzhz" : true
},
"availableat" : "gtgtgtgt",
"specialabout" : "jujujuju",
"development" : {
"predecessor" : "",
"follower" : "rfrfr"
},
"media" : [
]
}
My Clinder.cs :
using MongoDB.Bson;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization.Attributes;
using System;
using System.Collections.Generic;
using System.Globalization;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
namespace protocol.server.API.Clients
{
public class Cylinder
{
[BsonSerializer(typeof(ProductAttributeSerializer))]
public class ProductAttributeSerializer : IBsonSerializer, IBsonArraySerializer
{
public Type ValueType { get { return typeof(List<string>); } }
public object Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var type = context.Reader.GetCurrentBsonType();
List<String> items = new List<String>();
switch (type)
{
case BsonType.Document:
case BsonType.Array:
context.Reader.ReadStartArray();
while (context.Reader.ReadBsonType() != BsonType.EndOfDocument)
{
items.Add(context.Reader.ReadString());
}
context.Reader.ReadEndArray();
return new mode(items);
default:
throw new NotImplementedException($"No implementation to deserialize {type}");
}
}
public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value)
{
var d = value;
var attributes = value as List<string>;
if (attributes != null)
{
context.Writer.WriteStartArray();
foreach (string attr in attributes)
{
context.Writer.WriteString(attr);
}
context.Writer.WriteEndArray();
}
}
public bool TryGetItemSerializationInfo(out BsonSerializationInfo serializationInfo)
{
string elementName = null;
var serializer = BsonSerializer.LookupSerializer(typeof(string));
var nominalType = typeof(string);
serializationInfo = new BsonSerializationInfo(elementName, serializer, nominalType);
return true;
}
}
[BsonId]
public ObjectId Id { get; set; }
[BsonSerializer(typeof(ProductAttributeSerializer))]
public class mode
{
public mode(List<String> pItems)
{
this.items = new List<String>();
this.items.Clear();
this.items.AddRange(pItems);
}
public List<String> items { get; set; }
}
public class des
{
public string type { get; set; }
public string kind { get; set; }
public short year { get; set; }
public string producer { get; set; }
public List<string> brands { get; set; }
public string internalproducerdesignation { get; set; }
public string origin { get; set; }
public mode model { get; set; }
}
public class elem
{
public short nonspringelements { get; set; }
public short springelements { get; set; }
public short discelements { get; set; }
public short magneticelements { get; set; }
public short activeelements { get; set; }
public short passiveelements { get; set; }
public short totalelements { get; set; }
}
public des description = new des();
public elem elements = new elem();
public IEnumerable<string> profiles { get; set; }
public IEnumerable<string> certifications { get; set; }
public IEnumerable<string> colors { get; set; }
public IEnumerable<string> specialfittings { get; set; }
public short cutdepths { get; set; }
public string rareness { get; set; }
public class val
{
public String #new { get; set; }
public String used { get; set; }
}
public val value = new val();
public class Pi
{
public Double difficulty { get; set; }
public bool alreadypicked { get; set; }
}
public Pi Picking = new Pi();
public string availableat { get; set; }
public string specialabout { get; set; }
public class devel
{
public string predecessor { get; set; }
public string follower { get; set; }
}
public devel development = new devel();
public Object[] media;
}
}
How to prevent this error ? I just want to deserialize my objects...
while (context.Reader.ReadBsonType() != BsonType.EndOfDocument)
should be
while (context.Reader.State != BsonReaderState.Type || context.Reader.ReadBsonType() != BsonType.EndOfDocument)
Would cause to check the type if the state is a type. If it is not a type, you will pass and not check the type
Not sure why to use while loop there if you just want to fill properties of one object (description). You can do it like this:
public override Cylinder Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) {
context.Reader.ReadStartDocument();
Cylinder a = new Cylinder();
a.Id = context.Reader.ReadObjectId();
context.Reader.ReadStartDocument();
a.description.type = context.Reader.ReadString();
a.description.kind = context.Reader.ReadString();
a.description.year = (short) context.Reader.ReadInt32();
a.description.producer = context.Reader.ReadString();
return a;
}
Test (File bson.txt is copied verbatim from your question):
static void Main(string[] args) {
var cylinder = new CylinderSerializer().Deserialize(BsonDeserializationContext.CreateRoot(new BsonDocumentReader(BsonDocument.Parse(File.ReadAllText(#"G:\tmp\bson.txt")))));
Console.ReadKey();
}
It's a lot of job to write own serializer this way. This is how i did to for cylinder. I managed to deserialize your sample this way.
Please mention, that there is a one simple help method there to deserialize string array.
You don't have any class for "blaw" data, so i read it in not used variables.
public override Cylinder Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
context.Reader.ReadStartDocument();
Cylinder a = new Cylinder {Id = context.Reader.ReadObjectId()};
context.Reader.ReadStartDocument();
a.description.type = context.Reader.ReadString();
a.description.kind = context.Reader.ReadString();
a.description.year = (short)context.Reader.ReadInt32();
a.description.producer = context.Reader.ReadString();
context.Reader.ReadStartArray();
a.description.brands = new List<string>();
while (context.Reader.ReadBsonType() != BsonType.EndOfDocument)
{
a.description.brands.Add(context.Reader.ReadString());
}
context.Reader.ReadEndArray();
context.Reader.ReadStartArray();
a.description.model = new Cylinder.mode(new List<string>());
while (context.Reader.ReadBsonType() != BsonType.EndOfDocument)
{
a.description.model.items.Add(context.Reader.ReadString());
}
context.Reader.ReadEndArray();
a.description.internalproducerdesignation = context.Reader.ReadString();
a.description.origin = context.Reader.ReadString();
context.Reader.ReadEndDocument();
context.Reader.ReadStartDocument();
a.elements = new Cylinder.elem
{
nonspringelements = (short) context.Reader.ReadInt32(),
springelements = (short) context.Reader.ReadInt32(),
discelements = (short) context.Reader.ReadInt32(),
magneticelements = (short) context.Reader.ReadInt32(),
activeelements = (short) context.Reader.ReadInt32(),
passiveelements = (short) context.Reader.ReadInt32(),
totalelements = (short) context.Reader.ReadInt32()
};
context.Reader.ReadEndDocument();
a.profiles = readStringArray(context);
a.certifications = readStringArray(context);
a.colors = readStringArray(context);
a.specialfittings = readStringArray(context);
a.cutdepths = (short) context.Reader.ReadInt32();
a.rareness = context.Reader.ReadString();
context.Reader.ReadStartDocument();
a.value = new Cylinder.val
{
#new = context.Reader.ReadString(),
used = context.Reader.ReadString()
};
context.Reader.ReadEndDocument();
context.Reader.ReadStartDocument();
var blawInt = context.Reader.ReadDouble();
var blawBool = context.Reader.ReadBoolean();
context.Reader.ReadEndDocument();
a.availableat = context.Reader.ReadString();
a.specialabout = context.Reader.ReadString();
context.Reader.ReadStartDocument();
a.development = new Cylinder.devel
{
predecessor = context.Reader.ReadString(),
follower = context.Reader.ReadString()
};
context.Reader.ReadEndDocument();
var objects=new List<object>();
context.Reader.ReadStartArray();
while (context.Reader.ReadBsonType() != BsonType.EndOfDocument)
{
objects.Add(context.Reader.ReadString());
}
context.Reader.ReadEndArray();
a.media = objects.ToArray();
context.Reader.ReadEndDocument();
return a;
}
private static IEnumerable<string> readStringArray(BsonDeserializationContext context)
{
context.Reader.ReadStartArray();
var strings = new List<string>();
while (context.Reader.ReadBsonType() != BsonType.EndOfDocument)
{
strings.Add(context.Reader.ReadString());
}
context.Reader.ReadEndArray();
return strings;
}

JSON value is sometimes a string and sometimes an object

I have some JSON that can come in two different formats. Sometimes the location value is a string, and sometimes it is an object. This is a sample of the first format:
{
"result": [
{
"upon_approval": "Proceed to Next Task",
"location": "",
"expected_start": ""
}
]
}
Class definitions for this:
public class Result
{
public string upon_approval { get; set; }
public string location { get; set; }
public string expected_start { get; set; }
}
public class RootObject
{
public List<Result> result { get; set; }
}
Here is the JSON in the second format:
{
"result": [
{
"upon_approval": "Proceed to Next Task",
"location": {
"display_value": "Corp-HQR",
"link": "https://satellite.service-now.com/api/now/table/cmn_location/4a2cf91b13f2de00322dd4a76144b090"
},
"expected_start": ""
}
]
}
Class definitions for this:
public class Location
{
public string display_value { get; set; }
public string link { get; set; }
}
public class Result
{
public string upon_approval { get; set; }
public Location location { get; set; }
public string expected_start { get; set; }
}
public class RootObject
{
public List<Result> result { get; set; }
}
When deserializing, I get errors when the JSON format does not match my classes, but I don't know ahead of time which classes to use because the JSON format changes. So how can I dynamically get these two JSON formats to deserialize into one set of classes?
This is how I am deserializing now:
JavaScriptSerializer ser = new JavaScriptSerializer();
ser.MaxJsonLength = 2147483647;
RootObject ro = ser.Deserialize<RootObject>(responseValue);
To solve this problem you'll need to make a custom JavaScriptConverter class and register it with the serializer. The serializer will load the result data into a Dictionary<string, object>, then hand off to the converter, where you can inspect the contents and convert it into a usable object. In short, this will allow you to use your second set of classes for both JSON formats.
Here is the code for the converter:
class ResultConverter : JavaScriptConverter
{
public override IEnumerable<Type> SupportedTypes
{
get { return new List<Type> { typeof(Result) }; }
}
public override object Deserialize(IDictionary<string, object> dict, Type type, JavaScriptSerializer serializer)
{
Result result = new Result();
result.upon_approval = GetValue<string>(dict, "upon_approval");
var locDict = GetValue<IDictionary<string, object>>(dict, "location");
if (locDict != null)
{
Location loc = new Location();
loc.display_value = GetValue<string>(locDict, "display_value");
loc.link = GetValue<string>(locDict, "link");
result.location = loc;
}
result.expected_start = GetValue<string>(dict, "expected_start");
return result;
}
private T GetValue<T>(IDictionary<string, object> dict, string key)
{
object value = null;
dict.TryGetValue(key, out value);
return value != null && typeof(T).IsAssignableFrom(value.GetType()) ? (T)value : default(T);
}
public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
{
throw new NotImplementedException();
}
}
Then use it like this:
var ser = new JavaScriptSerializer();
ser.MaxJsonLength = 2147483647;
ser.RegisterConverters(new List<JavaScriptConverter> { new ResultConverter() });
RootObject ro = serializer.Deserialize<RootObject>(responseValue);
Here is a short demo:
class Program
{
static void Main(string[] args)
{
string json = #"
{
""result"": [
{
""upon_approval"": ""Proceed to Next Task"",
""location"": {
""display_value"": ""Corp-HQR"",
""link"": ""https://satellite.service-now.com/api/now/table/cmn_location/4a2cf91b13f2de00322dd4a76144b090""
},
""expected_start"": """"
}
]
}";
DeserializeAndDump(json);
Console.WriteLine(new string('-', 40));
json = #"
{
""result"": [
{
""upon_approval"": ""Proceed to Next Task"",
""location"": """",
""expected_start"": """"
}
]
}";
DeserializeAndDump(json);
}
private static void DeserializeAndDump(string json)
{
var serializer = new JavaScriptSerializer();
serializer.RegisterConverters(new List<JavaScriptConverter> { new ResultConverter() });
RootObject obj = serializer.Deserialize<RootObject>(json);
foreach (var result in obj.result)
{
Console.WriteLine("upon_approval: " + result.upon_approval);
if (result.location != null)
{
Console.WriteLine("location display_value: " + result.location.display_value);
Console.WriteLine("location link: " + result.location.link);
}
else
Console.WriteLine("(no location)");
}
}
}
public class RootObject
{
public List<Result> result { get; set; }
}
public class Result
{
public string upon_approval { get; set; }
public Location location { get; set; }
public string expected_start { get; set; }
}
public class Location
{
public string display_value { get; set; }
public string link { get; set; }
}
Output:
upon_approval: Proceed to Next Task
location display_value: Corp-HQR
location link: https://satellite.service-now.com/api/now/table/cmn_location/4a2cf91b13f2de00322dd4a76144b090
----------------------------------------
upon_approval: Proceed to Next Task
(no location)

Deserialize nested ICollection<BaseType> in Asp.Net Web API 2 controller

I have a Web Api Controller like this one :
public IHttpActionResult Create(PaymentDTO Payment)
My DTOs are:
public class PaymentDTO
{
public int Id { get; set; }
public string type { get; set; }
public IEnumerable<TransactionDTO> Transactions { get; set; }
}
public class TransactionDTO
{
public int Id { get; set; }
public string Description { get; set; }
public string CreateTime { get; set; }
public string UpdateTime { get; set; }
}
public class SaleDTO : TransactionDTO
{
public string Total { get; set; }
public string Currency{ get; set; }
}
public class OrderDTO : TransactionDTO
{
public string State {get;set;}
}
I receive the following JSON formatted data :
{
"Type": "sale",
"Id": 101,
"transactions": [
{
"Total": "30.50",
"Currency": "USD",
"Description": "transaction description"
}
]
}
I want JSON.net to instantiate either a IEnumerable<SaleDTO> or IEnumerable<OrderDTO> based on the Type Property.
I could've used a custom type converter, but only if Type property was in TransactionDTO. But I want the Type property to be in the parent object (PaymentDTO)
Thank you in advance for your help.
You can do this with a custom JsonConverter on the PaymentDTO class:
public class PaymentDTOConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(PaymentDTO).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var obj = JObject.Load(reader);
var payment = (PaymentDTO)existingValue ?? new PaymentDTO();
// Extract the transactions.
var transactions = obj.Property("transactions") ?? obj.Property("Transactions");
if (transactions != null)
transactions.Remove();
// Populate the remaining regular properties.
using (var subReader = obj.CreateReader())
serializer.Populate(subReader, payment);
if (transactions != null)
{
// Deserialize the transactions list.
var type = PaymentDTO.GetTransactionDTOType(payment.type) ?? typeof(TransactionDTO);
using (var subReader = transactions.Value.CreateReader())
// Here we are taking advantage of array covariance.
payment.Transactions = (IEnumerable<TransactionDTO>)serializer.Deserialize(subReader, type.MakeArrayType());
}
return payment;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Then apply it to your PaymentDTO class as follows:
[JsonConverter(typeof(PaymentDTOConverter))]
public class PaymentDTO
{
static Dictionary<string, Type> namesToTransactions;
static Dictionary<Type, string> transactionsToNames = new Dictionary<Type, string>
{
{ typeof(SaleDTO), "sale" },
{ typeof(OrderDTO), "order" },
};
static PaymentDTO()
{
namesToTransactions = transactionsToNames.ToDictionary(p => p.Value, p => p.Key);
}
public static string GetTransactionDTOTypeName<TTransactionDTO>() where TTransactionDTO : TransactionDTO
{
string name;
if (transactionsToNames.TryGetValue(typeof(TTransactionDTO), out name))
return name;
return null;
}
public static Type GetTransactionDTOType(string name)
{
Type type;
if (namesToTransactions.TryGetValue(name, out type))
return type;
return null;
}
public int Id { get; set; }
public string type { get; set; }
[JsonProperty("transactions")]
public IEnumerable<TransactionDTO> Transactions { get; set; }
}

How to configure JSON.net deserializer to track missing properties?

Sample class:
public class ClassA
{
public int Id { get; set; }
public string SomeString { get; set; }
public int? SomeInt { get; set; }
}
Default deserializer:
var myObject = JsonConvert.DeserializeObject<ClassA>(str);
Create the same object for two different inputs
{"Id":5}
or
{"Id":5,"SomeString":null,"SomeInt":null}
How can I track properties that were missing during deserialization process and preserve the same behavior? Is there a way to override some of JSON.net serializer methods (e.g. DefaultContractResolver class methods) to achive this. For example:
List<string> missingProps;
var myObject = JsonConvert.DeserializeObject<ClassA>(str, settings, missingProps);
For the first input list should contains the names of the missing properties ("SomeString", "SomeInt") and for second input it should be empty. Deserialized object remains the same.
1. JSON has a property which is missing in your class
Using property JsonSerializerSettings.MissingMemberHandling you can say whether missing properties are handled as errors.
Than you can install the Error delegate which will register errors.
This will detect if there is some "garbage" property in JSON string.
public class ClassA
{
public int Id { get; set; }
public string SomeString { get; set; }
}
internal class Program
{
private static void Main(string[] args)
{
const string str = "{'Id':5, 'FooBar': 42 }";
var myObject = JsonConvert.DeserializeObject<ClassA>(str
, new JsonSerializerSettings
{
Error = OnError,
MissingMemberHandling = MissingMemberHandling.Error
});
Console.ReadKey();
}
private static void OnError(object sender, ErrorEventArgs args)
{
Console.WriteLine(args.ErrorContext.Error.Message);
args.ErrorContext.Handled = true;
}
}
2. Your class has a property which is missing in JSON
Option 1:
Make it a required property:
public class ClassB
{
public int Id { get; set; }
[JsonProperty(Required = Required.Always)]
public string SomeString { get; set; }
}
Option 2:
Use some "special" value as a default value and check afterwards.
public class ClassB
{
public int Id { get; set; }
[DefaultValue("NOTSET")]
public string SomeString { get; set; }
public int? SomeInt { get; set; }
}
internal class Program
{
private static void Main(string[] args)
{
const string str = "{ 'Id':5 }";
var myObject = JsonConvert.DeserializeObject<ClassB>(str
, new JsonSerializerSettings
{
DefaultValueHandling = DefaultValueHandling.Populate
});
if (myObject.SomeString == "NOTSET")
{
Console.WriteLine("no value provided for property SomeString");
}
Console.ReadKey();
}
}
Option 3:
Another good idea would be to encapsulate this check iside the class istself. Create a Verify() method as shown below and call it after deserialization.
public class ClassC
{
public int Id { get; set; }
[DefaultValue("NOTSET")]
public string SomeString { get; set; }
public int? SomeInt { get; set; }
public void Verify()
{
if (SomeInt == null ) throw new JsonSerializationException("SomeInt not set!");
if (SomeString == "NOTSET") throw new JsonSerializationException("SomeString not set!");
}
}
Another way to find null/undefined tokens during De-serialization is to write a custom JsonConverter , Here is an example of custom converter which can report both omitted tokens (e.g. "{ 'Id':5 }") and null tokens (e.g {"Id":5,"SomeString":null,"SomeInt":null})
public class NullReportConverter : JsonConverter
{
private readonly List<PropertyInfo> _nullproperties=new List<PropertyInfo>();
public bool ReportDefinedNullTokens { get; set; }
public IEnumerable<PropertyInfo> NullProperties
{
get { return _nullproperties; }
}
public void Clear()
{
_nullproperties.Clear();
}
public override bool CanConvert(Type objectType)
{
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
existingValue = existingValue ?? Activator.CreateInstance(objectType, true);
var jObject = JObject.Load(reader);
var properties =
objectType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
foreach (var property in properties)
{
var jToken = jObject[property.Name];
if (jToken == null)
{
_nullproperties.Add(property);
continue;
}
var value = jToken.ToObject(property.PropertyType);
if(ReportDefinedNullTokens && value ==null)
_nullproperties.Add(property);
property.SetValue(existingValue, value, null);
}
return existingValue;
}
//NOTE: we can omit writer part if we only want to use the converter for deserializing
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var objectType = value.GetType();
var properties =
objectType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
writer.WriteStartObject();
foreach (var property in properties)
{
var propertyValue = property.GetValue(value, null);
writer.WritePropertyName(property.Name);
serializer.Serialize(writer, propertyValue);
}
writer.WriteEndObject();
}
}
Note: we can omit the Writer part if we don't need to use it for serializing objects.
Usage Example:
class Foo
{
public int Id { get; set; }
public string SomeString { get; set; }
public int? SomeInt { get; set; }
}
class Program
{
static void Main(string[] args)
{
var nullConverter=new NullReportConverter();
Console.WriteLine("Pass 1");
var obj0 = JsonConvert.DeserializeObject<Foo>("{\"Id\":5, \"Id\":5}", nullConverter);
foreach(var p in nullConverter.NullProperties)
Console.WriteLine(p);
nullConverter.Clear();
Console.WriteLine("Pass2");
var obj1 = JsonConvert.DeserializeObject<Foo>("{\"Id\":5,\"SomeString\":null,\"SomeInt\":null}" , nullConverter);
foreach (var p in nullConverter.NullProperties)
Console.WriteLine(p);
nullConverter.Clear();
nullConverter.ReportDefinedNullTokens = true;
Console.WriteLine("Pass3");
var obj2 = JsonConvert.DeserializeObject<Foo>("{\"Id\":5,\"SomeString\":null,\"SomeInt\":null}", nullConverter);
foreach (var p in nullConverter.NullProperties)
Console.WriteLine(p);
}
}
I got this problem, but defaultValue was not solution due to POCO object. I think this is simpler approach than NullReportConverter.
There are three unit tests. Root is class that encapsulate whole json. Key is type of the Property. Hope this helps someone.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
namespace SomeNamespace {
[TestClass]
public class NullParseJsonTest {
[TestMethod]
public void TestMethod1()
{
string slice = "{Key:{guid:\"asdf\"}}";
var result = JsonConvert.DeserializeObject<Root>(slice);
Assert.IsTrue(result.OptionalKey.IsSet);
Assert.IsNotNull(result.OptionalKey.Value);
Assert.AreEqual("asdf", result.OptionalKey.Value.Guid);
}
[TestMethod]
public void TestMethod2()
{
string slice = "{Key:null}";
var result = JsonConvert.DeserializeObject<Root>(slice);
Assert.IsTrue(result.OptionalKey.IsSet);
Assert.IsNull(result.OptionalKey.Value);
}
[TestMethod]
public void TestMethod3()
{
string slice = "{}";
var result = JsonConvert.DeserializeObject<Root>(slice);
Assert.IsFalse(result.OptionalKey.IsSet);
Assert.IsNull(result.OptionalKey.Value);
}
}
class Root {
public Key Key {
get {
return OptionalKey.Value;
}
set {
OptionalKey.Value = value;
OptionalKey.IsSet = true; // This does the trick, it is never called by JSON.NET if attribute missing
}
}
[JsonIgnore]
public Optional<Key> OptionalKey = new Optional<Key> { IsSet = false };
};
class Key {
public string Guid { get; set; }
}
class Optional<T> {
public T Value { get; set; }
public bool IsSet { get; set; }
}
}

Categories

Resources