Parse A Json String With Unpredictable-Typed Fields
It's a common way to exchange data between one service and another by delivering json string.
So, this example will show some methods about implementation for transforming a json string to an object (aka. deserialize).
Let us dive into it.
Basic Usage
Here is an example,
// we use a datetime formating string following ISO 8601-1:2019
const string sampleJson = "{\"DXIDS\":[{\"DXID\":\"123\"}],\"LICSNO\":\"**X-1234\",\"RTPTML\":10001,\"SRVNM\":\"服務人員\",\"GVCARDT\":\"2020-05-14T14:00:00+08:00\",\"RTPTDT\":\"20200514\"}";
Create a POCO model for mapping.
internal class Sample
{
public IEnumerable<Dxid> DXIDS { get; set; }
public string LICSNO { get; set; }
public int RTPTML { get; set; }
public string SRVNM { get; set; }
public DateTime GVCARDT { get; set; }
public string RTPTDT { get; set; }
}
internal class Dxid
{
public string DXID { get; set; }
}
Using a Deserializer to parse jsong string, and map to a specific object, System.Text.Json, and Newtonsoft.Json both can do that well.
private static void BasicWay()
{
// auto map properties by a POCO mapper
var result = System.Text.Json.JsonSerializer.Deserialize<Sample>(sampleJson);
var result2 = Newtonsoft.Json.JsonConvert.DeserializeObject<Sample>(sampleJson);
}
Another way, parse without a POCO model
Is there another way to parse json string ? Yes, sure, Newtonsoft.Json support Dynamic JSON Parsing to help us read json value.
private static void ParseWithoutAPoco()
{
// don't forget to specify **dynamic**, if using a *var*, that won't work (did you know why ?)
// for this example, we can also try **dynamic data = JObject.Parse(sampleJson);**, because the sampleJson is exectly an {} Json object
// while the sampleJson is exectly a [] Json array, we need to use *dynamic data = JArray.Parse(sampleJson);*
dynamic data = JValue.Parse(sampleJson);
// get fields each by each
// BUT, by using this way will NOT support by intellisense
var licenseNo = (string)data.LICSNO;
var dxid = (string)data.DXIDS.First?.DXID;
}
Unfortunately, System.Text.Json did NOT support this kind of low level reader yet. But we believe that it will do.
Everything Is Unpredictable
One day, in my current work, we have a input shown blow,
const string unpredictableJsonString = "[{\"NO\": \"1\", \"CNT\": 1, \"AMOUNT\": 100}]";
base on inputs, we create a POCO model to carry data.
internal class UnpredictableSample
{
public string NO { get; set; }
public int CNT { get; set; }
public int AMOUNT { get; set; }
}
However, things went south. We noticed that the parsing function will throw exception sometimes. Tracking data logs, and discussing with client, they told us it's difficult to limit end user's data input.
Errrrr... what was that?
usually, the regular inputs should be "[{"NO": "1", "CNT": 1, "AMOUNT": 100}]";
sometimes it will be "[{"NO": "1", "CNT": 1.0, "AMOUNT": 100}]";
occasionally it might be "[{"NO": "1", "CNT": "1 ", "AMOUNT": 100}]";, yes, as we can see, it's a string with many spacing...
What The F-word ?!
Since our client cannot correct the issue right away, we need to find another way to fix this trouble (shxt) here.
-System.Text.Json
We should change plan, change CNT field into dynamic type to carry data, and add new field named CNT_R (or other name you like) with int type to real-world apply property.
internal class UnpredictableSample
{
public string NO { get; set; }
public dynamic CNT { get; set; }
public int CNT_R { get; set; }
public int AMOUNT { get; set; }
}
const string unpredictableSampleJson = "[{\"NO\": \"1\", \"CNT\": 1, \"AMOUNT\": 100}, {\"NO\": \"1\", \"CNT\": 1.0, \"AMOUNT\": 100}, {\"NO\": \"1\", \"CNT\": \"1 \", \"AMOUNT\": 100}]";
Then, we will using JsonElement's ValueKind detecting to determine how CNT value will be converted.
private static void UnpredictableParsing()
{
var result = System.Text.Json.JsonSerializer.Deserialize<IEnumerable<UnpredictableSample>>(unpredictableSampleJson);
// parsing strategy
if (result?.Any() == true)
{
foreach (var item in result)
{
var jElementCNT = (System.Text.Json.JsonElement)item.CNT;
if (jElementCNT.ValueKind == System.Text.Json.JsonValueKind.Number)
{
// why GetDouble() or GetSingle() ? that's because of 1, or 1.0
item.CNT_R = (int)jElementCNT.GetSingle();
}
else if (jElementCNT.ValueKind == System.Text.Json.JsonValueKind.String)
{
int.TryParse(jElementCNT.GetString(), out var cnt);
item.CNT_R = cnt;
}
else
{
item.CNT_R = 0; // default to zero (according to faced situation)
}
}
}
}
-Newtonsoft.Json
Using a JValue / JArray we mention it earlier to parse data.
private static void UnpredictableParsing2()
{
// IEnumerable data = JValue.Parse(unpredictableSampleJson);
IEnumerable<dynamic> data = JArray.Parse(unpredictableSampleJson);
var ret = new List<UnpredictableSample>();
foreach (var item in data)
{
int cnt;
try
{
cnt = Convert.ToInt32(item.CNT);
}
catch
{
// try NOT parsing value from exception catching, it's a badass
// it might harm performance
int.TryParse((string)item.CNT, out cnt);
}
ret.Add(new UnpredictableSample {
NO = item.NO,
CNT_R = cnt,
AMOUNT = item.AMOUNT
});
}
}
See, it worked, we got our data parsed successfully.
In my personal opinion, I would like to choose the parsing method named UnpredictableParsing(), because of the intellisense supporting. It will be a great help for our working performance.
Try runnable samples on .Net fiddle
Enjoy it!
References:
How to serialize and deserialize (marshal and unmarshal) JSON in .NET