Skip to content

Instantly share code, notes, and snippets.

@lucd
Last active June 11, 2021 07:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lucd/cdd57a2602bd975ec0a6 to your computer and use it in GitHub Desktop.
Save lucd/cdd57a2602bd975ec0a6 to your computer and use it in GitHub Desktop.
This code shows how to specify a navigation path in the JsonProperty attribute. It will traverse the JSON node and obtain the desired value to according to the path. Example [JsonProperty(parent/child)] will map to the value "a" from the JSON { "parent":{ "child": "a"} }. http://stackoverflow.com/questions/35628318/deserialize-nested-json-to-a-f…
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Reflection;
namespace ConsoleApplication1
{
class Program
{
/// <summary>
/// This code shows how to specify a navigation path in the JsonProperty attribute.
/// It will traverse the JSON node and obtain the desired value to according to the path.
/// Example
/// [JsonProperty(parent/child)] will map to the value "a" from the JSON { "parent":{ "child": "a"} }.
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
string s = @"
{
""id"": 100,
""fields"":{
""issuetype"": {
""name"": ""I am a Jira issue""
}
}
}";
var settings = new JsonSerializerSettings();
settings.Converters.Add(new ConventionBasedConverter());
var o = JsonConvert.DeserializeObject<JiraIssue>(s, settings);
Console.WriteLine("Id: " + o.Id);
Console.WriteLine("Type: " + o.Type);
}
}
[JsonObject]
public class JiraIssue
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("fields/issuetype/name")]
public string Type { get; set; }
}
/// <summary>
/// Custom converter that allows mapping a JSON value according to a navigation path.
/// Credits to: http://stackoverflow.com/users/3887840/amit-kumar-ghosh
/// </summary>
class ConventionBasedConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(JiraIssue).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var daat = JObject.Load(reader);
var issue = new JiraIssue();
foreach (var prop in issue.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(issue, Convert.ToInt32(propValue));
}
else
{
prop.SetValue(issue, propValue);
}
}
}
}
}
return issue;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
}
}
}
@MinnowTman
Copy link

This version allows for the target class members to NOT have a JsonPropertyType attribute, and for the members to have other attributes besides just Json. Also tweaked CanConvert. I'm not sure what the original was up to. All I can say is that my version is what I needed. Made the target class a generic parameter. And I preferred a different name for the Converter class.

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Reflection;

namespace ConsoleApplication1
{
    /// <summary>
    /// Custom converter that allows mapping a JSON value according to a navigation path.
    /// </summary>

    public class FlattenNestedJSONConverter<T> : JsonConverter where T : new()
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof(T);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var daat = JObject.Load(reader);
            var result = new T();

            foreach (var prop in result.GetType().GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance))
            {
                string propName = string.Empty;
                //filter out non-Json attributes
                var attr = prop.GetCustomAttributes(false).Where(a => a.GetType() == typeof(JsonPropertyAttribute)).FirstOrDefault();
                if (attr != null)
                {
                    propName = ((JsonPropertyAttribute)attr).PropertyName;
                }
                //If no JsonPropertyAttribute existed, or no PropertyName was set,
                //still attempt to deserialize the class member
                if (string.IsNullOrWhiteSpace(propName))
                {
                    propName = prop.Name;
                }
                //split by the delimiter, and traverse recursevly according to the path
                var nests = propName.Split('/');
                object propValue = null;
                JToken token = null;
                for (var i = 0; i < nests.Length; i++)
                {
                    if (token == null)
                    {
                        token = daat[nests[i]];
                    }
                    else
                    {
                        token = token[nests[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(result, Convert.ToInt32(propValue));
                    }
                    else
                    {
                        prop.SetValue(result, propValue);
                    }
                }
            }
            return result;
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
        }
    }
}

@dst3p
Copy link

dst3p commented Aug 6, 2018

@MinnowTman your solution is basically what I needed. Glad you already did a lot of the work for me :)

@erick-thompson
Copy link

@MinnorTman Thanks for putting this together. It works fine for simple types, but doesn't work if the property is an object. For example, the following does not deserialize correctly - Person is null and WorkerId is not. If I don't use FlattenNestedJSONConverter, then Person is not null, but WorkerId is. Any thoughts on how to fix this? I'm happy to take a stab at it if you don't have the solution on the top of your head.

[JsonObject]
public class Worker
{
    [JsonProperty(PropertyName = "workerId/idValue")]
    public string WorkerId { get; set; }

    [JsonProperty(PropertyName = "person")]
    public Person Person { get; set; }
}

[JsonObject]
public class Person
{
    [JsonProperty(PropertyName = "legalName")]
    public string LegalName { get; set; }
}

@ameerkahiri
Copy link

SUPERBB!! Really helpful. Been search for solution yet this one is the best. Thank you!!

@ahmad-rzk
Copy link

What about a dynamic attributes :-
{"root":{ "787<--This attribute name is dynamic-->":"Value" }.... }

@Geoneer
Copy link

Geoneer commented Mar 27, 2019

What about a dynamic attributes :-
{"root":{ "787<--This attribute name is dynamic-->":"Value" }.... }

What do you mean with this?

@zipben
Copy link

zipben commented Apr 29, 2020

Thank you for this. I spent 20 minutes looking at answers on SO that danced around this simple solution.

@stevemerckel
Copy link

stevemerckel commented Nov 18, 2020

@MinnowTman - thank you! That gist really helped me out.

@SerhiyBalan
Copy link

I've posted a solution with classes support here (based on @MinnowTman code)
https://stackoverflow.com/a/67932675/4601817

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment