Can I specify a path in an attribute to map a property in my class to a child property in my JSON?
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
How to deserialize a property of a child JSON object to a property in the parent model using System.Text.Json?
I ended up going the converter route.
Recap of input JSON
{
"chapters": [
{
"id": 0,
"tags": {
"title": "Chapter 1"
}
},
{
"id": 1,
"tags": {
"title": "Chapter 2"
}
}
]
}
My converter class
public class JsonChapterTitleConverter : JsonConverter<string>
{
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var jsonTags = JsonDocument.ParseValue(ref reader);
var jsonTitle = jsonTags.RootElement.GetProperty("title");
return jsonTitle.GetString();
}
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => throw new NotImplementedException();
}
New model
public class Chapter
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("tags")]
[JsonConverter(typeof(JsonChapterTitleConverter))]
public string Title { get; set; }
}
The code used to deserialize the JSON is unchanged
I like the answer Serge gave. In this case however, I feel like it violates the Open-Closed Principle (if I understand it correctly). The JSON shown is not the full string; only what is important to me at the moment. If I decide I need to deserialize more of the elements; not only would I have to extend my model, but also modify the deserialization method in my service class. Using the converter means I only have to add another property to my model corresponding to the JSON property I want to capture.
While Maksym's answer isn't a complete answer, the link provided gave me the boost I needed to figure this out.
Thank you.
Comments welcome.
Json.NET Deserialize Property Without Deserializing Parent Object Above It
I suggest two approaches that are very explicit and easy to follow for the next developer looking at the code.
Two classes
creating a intermediate dto class that is used for deserialisation and then creating the business logic object from that intermediate object.
var withAttributes = Deserialise<ListingDto>();
var flatObject = new Listing(withAttributes);
One class
You could provide accessors at the top level which dip into the subclasses.
public class Listing
{
public AttributesDto Attributes {get; set}
...
public string Url => Attributes.Url; // Maybe '?.Url'
}
How to map a nested value to a property using Jackson annotations?
You can achieve this like that:
String brandName;
@JsonProperty("brand")
private void unpackNameFromNestedObject(Map<String, String> brand) {
brandName = brand.get("name");
}
Related Topics
How to Export Datatable to Excel
Find a Private Field With Reflection
How to Ensure That a Division of Integers Is Always Rounded Up
Redirect Console Output to Textbox in Separate Program
Why Does One Often See "Null != Variable" Instead of "Variable != Null" in C#
How to Run Servicestack on Linux/Mono
Difference Between Shadowing and Overriding in C#
How to Cast Object of Type 'System.Dbnull' to Type 'System.String'
How to String.Format a Timespan Object With a Custom Format in .Net
Why Doesn't C# Allow Static Methods to Implement an Interface