69

There is some code (which I can't change) that uses Newtonsoft.Json's DeserializeObject<T>(strJSONData) to take data from a web request and convert it to a class object (I can change the class). By decorating my class properties with [DataMember(Name = "raw_property_name")] I can map the raw JSON data to the correct property in my class. Is there a way I can map the child property of a JSON complex object to a simple property? Here's an example:

{
    "picture": 
    {
        "id": 123456,
        "data": 
        {
            "type": "jpg",
            "url": "http://www.someplace.com/mypicture.jpg"
        }
    }
}

I don't care about any of the rest of the picture object except for URL, and so don't want to setup a complex object in my C# class. I really just want something like:

[DataMember(Name = "picture.data.url")]
public string ProfilePicture { get; set; }

Is this possible?

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
David P
  • 2,027
  • 3
  • 15
  • 27
  • The best answer to this I found it here: https://stackoverflow.com/questions/52619432/json-net-deserialize-object-nested-data In case someone wants to check it out! – letie Apr 07 '21 at 14:04

7 Answers7

85

Well, if you just need a single extra property, one simple approach is to parse your JSON to a JObject, use ToObject() to populate your class from the JObject, and then use SelectToken() to pull in the extra property.

So, assuming your class looked something like this:

class Person
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("age")]
    public string Age { get; set; }

    public string ProfilePicture { get; set; }
}

You could do this:

string json = @"
{
    ""name"" : ""Joe Shmoe"",
    ""age"" : 26,
    ""picture"":
    {
        ""id"": 123456,
        ""data"":
        {
            ""type"": ""jpg"",
            ""url"": ""http://www.someplace.com/mypicture.jpg""
        }
    }
}";

JObject jo = JObject.Parse(json);
Person p = jo.ToObject<Person>();
p.ProfilePicture = (string)jo.SelectToken("picture.data.url");

Fiddle: https://dotnetfiddle.net/7gnJCK


If you prefer a more fancy solution, you could make a custom JsonConverter to enable the JsonProperty attribute to behave like you describe. The converter would need to operate at the class level and use some reflection combined with the above technique to populate all the properties. Here is what it might look like in code:

class JsonPathConverter : JsonConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, 
                                    object existingValue, JsonSerializer serializer)
    {
        JObject jo = JObject.Load(reader);
        object targetObj = Activator.CreateInstance(objectType);

        foreach (PropertyInfo prop in objectType.GetProperties()
                                                .Where(p => p.CanRead && p.CanWrite))
        {
            JsonPropertyAttribute att = prop.GetCustomAttributes(true)
                                            .OfType<JsonPropertyAttribute>()
                                            .FirstOrDefault();

            string jsonPath = (att != null ? att.PropertyName : prop.Name);
            JToken token = jo.SelectToken(jsonPath);

            if (token != null && token.Type != JTokenType.Null)
            {
                object value = token.ToObject(prop.PropertyType, serializer);
                prop.SetValue(targetObj, value, null);
            }
        }

        return targetObj;
    }

    public override bool CanConvert(Type objectType)
    {
        // CanConvert is not called when [JsonConverter] attribute is used
        return false;
    }

    public override bool CanWrite
    {
        get { return false; }
    }

    public override void WriteJson(JsonWriter writer, object value,
                                   JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

To demonstrate, let's assume the JSON now looks like the following:

{
  "name": "Joe Shmoe",
  "age": 26,
  "picture": {
    "id": 123456,
    "data": {
      "type": "jpg",
      "url": "http://www.someplace.com/mypicture.jpg"
    }
  },
  "favorites": {
    "movie": {
      "title": "The Godfather",
      "starring": "Marlon Brando",
      "year": 1972
    },
    "color": "purple"
  }
}

...and you are interested in the person's favorite movie (title and year) and favorite color in addition to the information from before. You would first mark your target class with a [JsonConverter] attribute to associate it with the custom converter, then use [JsonProperty] attributes on each property, specifying the desired property path (case sensitive) as the name. The target properties don't have to be primitives either-- you can use a child class like I did here with Movie (and notice there's no intervening Favorites class required).

[JsonConverter(typeof(JsonPathConverter))]
class Person
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("age")]
    public int Age { get; set; }

    [JsonProperty("picture.data.url")]
    public string ProfilePicture { get; set; }

    [JsonProperty("favorites.movie")]
    public Movie FavoriteMovie { get; set; }

    [JsonProperty("favorites.color")]
    public string FavoriteColor { get; set; }
}

// Don't need to mark up these properties because they are covered by the 
// property paths in the Person class
class Movie
{
    public string Title { get; set; }
    public int Year { get; set; }
}

With all the attributes in place, you can just deserialize as normal and it should "just work":

Person p = JsonConvert.DeserializeObject<Person>(json);

Fiddle: https://dotnetfiddle.net/Ljw32O

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • 1
    I really like your "fancy" solution, but could you make it compatible for .NET 4.0? prop.GetCustomAttributes is saying it can't be used with type arguments, and token.ToObject is saying that no overload method takes 2 arguments. – David P Oct 13 '15 at 13:04
  • NM - I see that your Fiddle actually has compatible code for GetCustomAttributes. The ToObject problem is that I am using an older version of JSON.NET. – David P Oct 13 '15 at 14:34
  • 1
    Heh, that is because I just updated it to be compatible with 4.0 ;-) Also updated the code above. – Brian Rogers Oct 13 '15 at 14:38
  • If I want to use older version JSON.NET - would object value = token.ToObject(serializer); work? – David P Oct 13 '15 at 14:47
  • The 2-parameter issue was not with ToObject, it was with SetValue. You should have no problem with ToObject the way it is written, going back to 5.0.8 at least. – Brian Rogers Oct 13 '15 at 15:00
  • 1
    how would one go about serializing this back to a child property – Chris McGrath Mar 20 '16 at 17:00
  • 1
    @ChrisMcGrath I think you want what I added as an answer. – Cristiano Santos May 05 '16 at 09:34
  • 1
    This solution seems to break other JsonConverterAttribute applied on properties: they are no longer used automatically :/ – Melvyn Apr 28 '18 at 22:37
  • 1
    The `JsonPathConverter` class you provided is a good starting point, but it breaks if you have any `JsonConverter` attributes on any of the properties, and for anything that doesn't have a setter (e.g. collection based properties); I went ahead and put together a version that fixes those issues, based on this one. You can find it at: https://pastebin.com/4804DCzH – BrainSlugs83 Aug 31 '18 at 03:17
  • @BrainSlugs83, Thanks for your improved version. When using a UnixDateTimeConverter attribute on a property, I get a JsonSerializationException: "Unexpected token parsing date. Expected Integer or String, got None.". The exception happens when calling converter.ReadJson(). – teisnet Nov 03 '22 at 12:58
19

The marked answer is not 100% complete as it ignores any IContractResolver that may be registered such as CamelCasePropertyNamesContractResolver etc.

Also returning false for can convert will prevent other user cases so i changed it to return objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().Any();

Here is the updated version: https://dotnetfiddle.net/F8C8U8

I also removed the need to set a JsonProperty on a property as illustrated in the link.

If for some reason the link above dies or explodes i also including the code below:

public class JsonPathConverter : JsonConverter
    {
        /// <inheritdoc />
        public override object ReadJson(
            JsonReader reader,
            Type objectType,
            object existingValue,
            JsonSerializer serializer)
        {
            JObject jo = JObject.Load(reader);
            object targetObj = Activator.CreateInstance(objectType);

            foreach (PropertyInfo prop in objectType.GetProperties().Where(p => p.CanRead && p.CanWrite))
            {
                JsonPropertyAttribute att = prop.GetCustomAttributes(true)
                                                .OfType<JsonPropertyAttribute>()
                                                .FirstOrDefault();

                string jsonPath = att != null ? att.PropertyName : prop.Name;

                if (serializer.ContractResolver is DefaultContractResolver)
                {
                    var resolver = (DefaultContractResolver)serializer.ContractResolver;
                    jsonPath = resolver.GetResolvedPropertyName(jsonPath);
                }

                if (!Regex.IsMatch(jsonPath, @"^[a-zA-Z0-9_.-]+$"))
                {
                    throw new InvalidOperationException($"JProperties of JsonPathConverter can have only letters, numbers, underscores, hiffens and dots but name was ${jsonPath}."); // Array operations not permitted
                }

                JToken token = jo.SelectToken(jsonPath);
                if (token != null && token.Type != JTokenType.Null)
                {
                    object value = token.ToObject(prop.PropertyType, serializer);
                    prop.SetValue(targetObj, value, null);
                }
            }

            return targetObj;
        }

        /// <inheritdoc />
        public override bool CanConvert(Type objectType)
        {
            // CanConvert is not called when [JsonConverter] attribute is used
            return objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().Any();
        }

        /// <inheritdoc />
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var properties = value.GetType().GetRuntimeProperties().Where(p => p.CanRead && p.CanWrite);
            JObject main = new JObject();
            foreach (PropertyInfo prop in properties)
            {
                JsonPropertyAttribute att = prop.GetCustomAttributes(true)
                    .OfType<JsonPropertyAttribute>()
                    .FirstOrDefault();

                string jsonPath = att != null ? att.PropertyName : prop.Name;

                if (serializer.ContractResolver is DefaultContractResolver)
                {
                    var resolver = (DefaultContractResolver)serializer.ContractResolver;
                    jsonPath = resolver.GetResolvedPropertyName(jsonPath);
                }

                var nesting = jsonPath.Split('.');
                JObject lastLevel = main;

                for (int i = 0; i < nesting.Length; i++)
                {
                    if (i == nesting.Length - 1)
                    {
                        lastLevel[nesting[i]] = new JValue(prop.GetValue(value));
                    }
                    else
                    {
                        if (lastLevel[nesting[i]] == null)
                        {
                            lastLevel[nesting[i]] = new JObject();
                        }

                        lastLevel = (JObject)lastLevel[nesting[i]];
                    }
                }
            }

            serializer.Serialize(writer, main);
        }
    }
robgha01
  • 383
  • 4
  • 11
  • I like that you added writable support and -- I may have to borrow that from you in my own implementation. Though you may want to borrow the reading support from mine, as yours does not support properties without setters (e.g. the best practice for working with collections). -- Mine is located at: https://pastebin.com/4804DCzH – BrainSlugs83 Aug 31 '18 at 03:22
10

Istead of doing

lastLevel [nesting [i]] = new JValue(prop.GetValue (value));

You have to do

lastLevel[nesting[i]] = JValue.FromObject(jValue);

Otherwise we have a

Could not determine JSON object type for type ...

exception

A complete piece of code would be this:

object jValue = prop.GetValue(value);
if (prop.PropertyType.IsArray)
{
    if(jValue != null)
        //https://stackoverflow.com/a/20769644/249895
        lastLevel[nesting[i]] = JArray.FromObject(jValue);
}
else
{
    if (prop.PropertyType.IsClass && prop.PropertyType != typeof(System.String))
    {
        if (jValue != null)
            lastLevel[nesting[i]] = JValue.FromObject(jValue);
    }
    else
    {
        lastLevel[nesting[i]] = new JValue(jValue);
    }                               
}
Dragos Durlut
  • 8,018
  • 10
  • 47
  • 62
  • object jValue = prop.GetValue(value); – Dragos Durlut Dec 17 '18 at 14:48
  • 1
    I found that it appears that you can avoid the conditional code above by using `JToken.FromObject()` instead. However, it also appears that there is what appears to be a fatal flaw in the overall approach in that `FromObject()` doesn't recursively call the `JsonConverter`. So, if you have an array that contains objects that also have names that are JSON paths, it won't handle them correctly. – Jon Miller Sep 22 '20 at 06:55
5

If someone needs to use the JsonPathConverter of @BrianRogers also with the WriteJson option, here's a solution (that works only for paths with dots only):

Remove the CanWrite property so that it becomes true by default again.

Replace WriteJson code by the following:

public override void WriteJson(JsonWriter writer, object value,
    JsonSerializer serializer)
{
    var properties = value.GetType().GetRuntimeProperties ().Where(p => p.CanRead && p.CanWrite);
    JObject main = new JObject ();
    foreach (PropertyInfo prop in properties) {
        JsonPropertyAttribute att = prop.GetCustomAttributes(true)
            .OfType<JsonPropertyAttribute>()
            .FirstOrDefault();

        string jsonPath = (att != null ? att.PropertyName : prop.Name);
        var nesting=jsonPath.Split(new[] { '.' });
        JObject lastLevel = main;
        for (int i = 0; i < nesting.Length; i++) {
            if (i == nesting.Length - 1) {
                lastLevel [nesting [i]] = new JValue(prop.GetValue (value));
            } else {
                if (lastLevel [nesting [i]] == null) {
                    lastLevel [nesting [i]] = new JObject ();
                }
                lastLevel = (JObject)lastLevel [nesting [i]];
            }
        }

    }
    serializer.Serialize (writer, main);
}

As I said above, this only works for paths that contains dots. Given that, you should add the following code to ReadJson in order to prevent other cases:

[...]
string jsonPath = (att != null ? att.PropertyName : prop.Name);
if (!Regex.IsMatch(jsonPath, @"^[a-zA-Z0-9_.-]+$")) {
    throw new InvalidOperationException("JProperties of JsonPathConverter can have only letters, numbers, underscores, hiffens and dots."); //Array operations not permitted
}
JToken token = jo.SelectToken(jsonPath);
[...]
Cristiano Santos
  • 2,157
  • 2
  • 35
  • 53
1

Another solution (original source code was taken from https://gist.github.com/lucd/cdd57a2602bd975ec0a6). I've cleaned source codes and added classes / arrays of classes support. Requires C# 7

/// <summary>
/// Custom converter that allows mapping a JSON value according to a navigation path.
/// </summary>
/// <typeparam name="T">Class which contains nested properties.</typeparam>
public class NestedJsonConverter<T> : JsonConverter
    where T : new()
{
    /// <inheritdoc />
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(T);
    }

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

        // Get all properties of a provided class
        var properties = result
            .GetType()
            .GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance);

        foreach (var propertyInfo in properties)
        {
            var jsonPropertyAttribute = propertyInfo
                .GetCustomAttributes(false)
                .FirstOrDefault(attribute => attribute is JsonPropertyAttribute);

            // Use either custom JSON property or regular property name
            var propertyName = jsonPropertyAttribute != null
                ? ((JsonPropertyAttribute)jsonPropertyAttribute).PropertyName
                : propertyInfo.Name;

            if (string.IsNullOrEmpty(propertyName))
            {
                continue;
            }

            // Split by the delimiter, and traverse recursively according to the path
            var names = propertyName.Split('/');
            object propertyValue = null;
            JToken token = null;
            for (int i = 0; i < names.Length; i++)
            {
                var name = names[i];
                var isLast = i == names.Length - 1;

                token = token == null
                    ? data.GetValue(name, StringComparison.OrdinalIgnoreCase)
                    : ((JObject)token).GetValue(name, StringComparison.OrdinalIgnoreCase);

                if (token == null)
                {
                    // Silent fail: exit the loop if the specified path was not found
                    break;
                }

                if (token is JValue || token is JArray || (token is JObject && isLast))
                {
                    // simple value / array of items / complex object (only if the last chain)
                    propertyValue = token.ToObject(propertyInfo.PropertyType, serializer);
                }
            }

            if (propertyValue == null)
            {
                continue;
            }

            propertyInfo.SetValue(result, propertyValue);
        }

        return result;
    }

    /// <inheritdoc />
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
    }
}

Sample model

public class SomeModel
{
    public List<string> Records { get; set; }

    [JsonProperty("level1/level2/level3")]
    public string SomeValue{ get; set; }
}

sample json:

{
    "records": ["some value1", "somevalue 2"],
    "level1":
    {
         "level2":
         {
             "level3": "gotcha!"
         }
    }
}

Once you have added a JsonConverter, you can use it like this:

var json = "{}"; // input json string
var settings = new JsonSerializerSettings();
settings.Converters.Add(new NestedJsonConverter<SomeModel>());
var result = JsonConvert.DeserializeObject<SomeModel>(json , settings);

Fiddle: https://dotnetfiddle.net/pBK9dj

Keep mind that if you have several nested properties in different classes then you would need to add as many converters as many classes you have:

settings.Converters.Add(new NestedJsonConverter<Model1>());
settings.Converters.Add(new NestedJsonConverter<Model2>());
...
DonSleza4e
  • 562
  • 6
  • 11
1

FYI, I added a little extra to account for any other converts on the nested property. For example, we had a nested DateTime? property, but the result was sometimes provided as an empty string, so we had to have another JsonConverter which accommodated for this.

Our class ended up like this:

[JsonConverter(typeof(JsonPathConverter))] // Reference the nesting class
public class Timesheet {

    [JsonConverter(typeof(InvalidDateConverter))]
    [JsonProperty("time.start")]
    public DateTime? StartTime { get; set; }

}

The JSON was:


{
    time: {
        start: " "
    }
}

The final update to the JsonConverter above is:

var token = jo.SelectToken(jsonPath);
                if (token != null && token.Type != JTokenType.Null)
                {
                    object value = null;

                    // Apply custom converters
                    var converters = prop.GetCustomAttributes<JsonConverterAttribute>(); //(true).OfType<JsonPropertyAttribute>().FirstOrDefault();
                    if (converters != null && converters.Any())
                    {
                        foreach (var converter in converters)
                        {
                            var converterType = (JsonConverter)Activator.CreateInstance(converter.ConverterType);
                            if (!converterType.CanRead) continue;
                            value = converterType.ReadJson(token.CreateReader(), prop.PropertyType, value, serializer);
                        }
                    }
                    else
                    {
                        value = token.ToObject(prop.PropertyType, serializer);
                    }


                    prop.SetValue(targetObj, value, null);
                }
Ben
  • 77
  • 11
  • I tried your solution, since I'm using a UnixDateTimeConverter attribute on a property, but I get a JsonSerializationException: "Unexpected token parsing date. Expected Integer or String, got None." when parsing that property. The exception happens at converter.ReadJson(). – teisnet Nov 03 '22 at 13:09
  • 1
    (Solution:) You need to call Read() on the object returnedfrom token.CreateReader() before passing it to converterType.ReadJson() – teisnet Nov 05 '22 at 21:49
0

With help of all the answers in this thread I came up with solution of JsonPathConverter class (used as JsonConverter attribute) which implements both ReadJson and WriteJson that works with forward slashes.

The class implementation:

/// <summary>
/// Custom converter that allows mapping a JSON value according to a navigation path using forward slashes "/".
/// </summary>
public class JsonPathConverter : JsonConverter
{
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject data = JObject.Load(reader);
        object resultObject = Activator.CreateInstance(objectType);

        // Get all properties of a provided class
        PropertyInfo[] properties = objectType
            .GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance);

        foreach (PropertyInfo propertyInfo in properties)
        {
            JsonPropertyAttribute propertyAttribute = propertyInfo
                .GetCustomAttributes(true)
                .OfType<JsonPropertyAttribute>()
                .FirstOrDefault();

            // Use either custom JSON property or regular property name
            string propertyJsonPath = propertyAttribute != null
                ? propertyAttribute.PropertyName
                : propertyInfo.Name;

            if (string.IsNullOrEmpty(propertyJsonPath))
            {
                continue;
            }

            // Split by the delimiter, and traverse recursively according to the path
            string[] nesting = propertyJsonPath.Split('/');
            object propertyValue = null;
            JToken token = null;
            for (int i = 0; i < nesting.Length; i++)
            {
                string name = nesting[i];
                bool isLast = i == nesting.Length - 1;

                token = token == null
                    ? data.GetValue(name, StringComparison.OrdinalIgnoreCase)
                    : ((JObject)token).GetValue(name, StringComparison.OrdinalIgnoreCase);

                if (token == null)
                {
                    // Silent fail: exit the loop if the specified path was not found
                    break;
                }

                if (token is JValue || token is JArray || (token is JObject && isLast))
                {
                    // simple value / array of items / complex object (only if the last chain)
                    propertyValue = token.ToObject(propertyInfo.PropertyType, serializer);
                }
            }

            if (propertyValue == null)
            {
                continue;
            }

            propertyInfo.SetValue(resultObject, propertyValue);
        }

        return resultObject;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        JObject resultJson = new();

        // Get all properties of a provided class
        IEnumerable<PropertyInfo> properties = value
            .GetType().GetRuntimeProperties().Where(p => p.CanRead && p.CanWrite);

        foreach (PropertyInfo propertyInfo in properties)
        {
            JsonPropertyAttribute propertyAttribute = propertyInfo
                .GetCustomAttributes(true)
                .OfType<JsonPropertyAttribute>()
                .FirstOrDefault();

            // Use either custom JSON property or regular property name
            string propertyJsonPath = propertyAttribute != null
                ? propertyAttribute.PropertyName
                : propertyInfo.Name;

            if (serializer.ContractResolver is DefaultContractResolver resolver)
            {
                propertyJsonPath = resolver.GetResolvedPropertyName(propertyJsonPath);
            }

            if (string.IsNullOrEmpty(propertyJsonPath))
            {
                continue;
            }

            // Split by the delimiter, and traverse according to the path
            string[] nesting = propertyJsonPath.Split('/');
            JObject lastJsonLevel = resultJson;
            for (int i = 0; i < nesting.Length; i++)
            {
                if (i == nesting.Length - 1)
                {
                    lastJsonLevel[nesting[i]] = JToken.FromObject(propertyInfo.GetValue(value));
                }
                else
                {
                    if (lastJsonLevel[nesting[i]] == null)
                    {
                        lastJsonLevel[nesting[i]] = new JObject();
                    }

                    lastJsonLevel = (JObject)lastJsonLevel[nesting[i]];
                }
            }
        }

        serializer.Serialize(writer, resultJson);
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType.GetCustomAttributes(true).OfType<JsonPathConverter>().Any();
    }
}

Please keep in mind you will also need these usings:

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

Usage of this custom JsonConverter is very simple. Let's say we have the OP's JSON:

{
    "picture":
    {
        "id": 123456,
        "data":
        {
            "type": "jpg",
            "url": "http://www.someplace.com/mypicture.jpg"
        }
    }
}

According to that, we can create object that will hold the JSON data:

[JsonConverter(typeof(JsonPathConverter))]
public class Picture
{
    [JsonProperty("id")]
    public int Id { get; set; }

    [JsonProperty("data/type")]
    public int Type { get; set; }

    [JsonProperty("data/url")]
    public string Url { get; set; }
}

NOTE: Don't forget to mark your target class with a JsonConverter attribute and specify the newly created JsonPathConverter converter as shown above.

Then just deserialize the JSON to our object as normal:

var picture = JsonConvert.DeserializeObject<Picture>(json);
B8ightY
  • 469
  • 5
  • 5
  • This one is almost perfect, except that when serializing an object, if the value of a field is null, it errors out. Modifying the code a bit to handle this edge case is trivial. ` if (i == nesting.Length - 1) { var val = propertyInfo.GetValue(value) ?? JValue.CreateNull(); lastJsonLevel[nesting[i]] = JToken.FromObject(val); } ` – Flojomojo Jul 22 '23 at 15:59