Parsing JSON based on path (No 3rd Party)

Working in any project these days will stumble you upon working with some external (API) data that mostly likely will have a JSON response type.
We talked about how to work with System.Text.Json to correctly parse data, which you can find it here.
Sometimes you want like one property out of a hole bunch of data while the rest is just not needed, but the property you want is baked so deep in the json that you need to write multiple map models. 🙁
But we can do better! Our aim is to create a method that would internally use baked in System libs to parse the data based on path or paths (which ever matches) with out the need to create many models.
Okay now, lets get some data and see what we aim to get.
{
"root": {
"field_string" : "Data for field1",
"field_int" : 2,
"field_root_object":{
"field_list" : [
{
"list_item_string" : "value inside list item",
"list_item_float" : 2.5,
"list_item_bool" : true,
"list_item_obj" : {
"list_item_obj_string" :"value inside list item object"
}
}
]
}
},
"page": {
"current" : 1,
"count": 5
}
}
Going through the json object is pretty simple just some root node which holds different types of fields.
Now lets say from the hole data we only need “filed_list” and we want to create minimum number of models to map to.
If we are using the backed-in tools that we have then we would need multiple model to get a correct map.
So lets get into it!
First for easy of use we want to make it based on an attribute so it would make our code look clean and we could give it to any property while specifying the path to its values.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class JsonPathAttrribute : Attribute
{
/// <summary>
/// A path to the json property spereated with a dot/'.'
/// </summary>
public string Path { get; private set; }
public JsonPathAttrribute(string path)
{
Path = path;
}
}
By defining an attribute we could use it as the following.
public class ParsingModel
{
[JsonPath("root.filed_int")]
public int FieldInt { get; set; }
[JsonPath("root.filed_root_object.field_list")]
public IEnumerable<object> FieldList { get; set; }
}
As clean as that we want it to be!
Now it is time for the real meat our parsing function.
public static object ParseJson(this string json, Type returnType)
{
if (json is null)
{
throw new ArgumentNullException(nameof(json));
}
//Create the object
var obj = Activator.CreateInstance(returnType);
try
{
//Try to parse the data
using (JsonDocument jd = JsonDocument.Parse(json))
{
//Get the root element
var root = jd.RootElement;
var objProperties = obj.GetType().GetProperties();
foreach (var _prop in objProperties)
{
var pathes = new List<string>();
//Chech if attribute was sent and if found
if (Attribute.IsDefined(_prop, typeof(JsonPathAttribute)))
{
pathes = Attribute.GetCustomAttributes(_prop, typeof(JsonPathAttribute)).Select(a => (a as JsonPathAttribute).Path).ToList();
}
else
{
pathes.Add(_prop.Name);
}
//The value to set for property
string _value = null;
var pathMatchDepth = new List<Tuple<int, string, JsonElement>>();
foreach (var _path in pathes)
{
var _r = root;
var splitPath = _path.Split('.');
int i = 0;
for (i = 0; i < splitPath.Length; i++)
{
var _sp = Char.ToLowerInvariant(splitPath[i][0]) + splitPath[i].Substring(1);
if (_r.TryGetProperty(_sp, out var _je))
{
_r = _je;
}
else
{
//Once a part of the sequance was not found we break and get the last point we were in
break;
}
}
pathMatchDepth.Add(new Tuple<int, string, JsonElement>(splitPath.Length - i, _path, _r));
}
_value = pathMatchDepth.OrderBy(p => p.Item1).First().Item3.GetRawText();
try
{
if (_prop.PropertyType == typeof(DateTime) && DateTime.TryParse(_value.Replace("\"", ""), out var _vdt))
{
_prop.SetValue(obj, _vdt);
}
else if (_prop.PropertyType.FullName.StartsWith("TestJsonPath"))
{
_prop.SetValue(obj, _value.ParseJson(_prop.PropertyType));
}
else if (_prop.PropertyType.IsClass || _prop.PropertyType.IsInterface)
{
_prop.SetValue(obj, JsonSerializer.Deserialize(_value, _prop.PropertyType));
}
else
{
_prop.SetValue(obj, Convert.ChangeType(_value, _prop.PropertyType));
}
}
catch (Exception) { }
}
}
}
catch (Exception)
{
throw;
}
return obj;
}
To explain the code what we do is get the path value that was defined in our custom attribute.
We create a list of tuples which include the (order, Match percentage, path).
Try all the paths that are defined in the attribute because we allowed for multiple ones.
Now going through the JsonDoument try to check if the path exists or break at the first stop.
Order the paths based on there match percentage.
Now comes the parsing, for datetime we try to parse the value while cleaning the string.
For any defined models at our end we check for the namespace and if so we path them through the same function as there could be some properties which are getting values by the path too.
For normal classes or Interfaces (IEnumerable) we use the baked in parser.
For any normal types we try to change its datatype.
And we are done!!!
By calling the function on our example up there we get the following output.

We successfully parsed the values based on there path.
You can set the attribute multiple times on the same property so it could find the best match and try to fill it up.
Hope you enjoyed the read!
No Comments