Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I specify a path in an attribute to map a property in my class to a child property in my JSON?

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?

like image 756
David P Avatar asked Oct 12 '15 19:10

David P


2 Answers

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

like image 152
Brian Rogers Avatar answered Oct 04 '22 02:10

Brian Rogers


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);         }     } 
like image 23
robgha01 Avatar answered Oct 04 '22 02:10

robgha01