I have the following HAL+JSON sample:
{
"id": "4a17d6fe-a617-4cf8-a850-0fb6bc8576fd",
"country": "DE",
"_embedded": {
"company": {
"name": "Apple",
"industrySector": "IT",
"owner": "Klaus Kleber",
"_embedded": {
"emailAddresses": [
{
"id": "4a17d6fe-a617-4cf8-a850-0fb6bc8576fd",
"value": "[email protected]",
"type": "Business",
"_links": {
"self": {
"href": "https://any-host.com/api/v1/customers/1234"
}
}
}
],
"phoneNumbers": [
{
"id": "4a17d6fe-a617-4cf8-a850-0fb6bc8576fd",
"value": "01670000000",
"type": "Business",
"_links": {
"self": {
"href": "https://any-host.com/api/v1/customers/1234"
}
}
}
],
},
"_links": {
"self": {
"href": "https://any-host.com/api/v1/customers/1234"
},
"phoneNumbers": {
"href": "https://any-host.com/api/v1/customers/1234"
},
"addresses": {
"href": "https://any-host.com/api/v1/customers/1234"
},
}
},
},
"_links": {
"self": {
"href": "https://any-host.com/api/v1/customers/1234"
},
"legalPerson": {
"href": "https://any-host.com/api/v1/customers/1234"
},
"naturalPerson": {
"href": "https://any-host.com/api/v1/customers/1234"
}
}
}
And the following models:
public class Customer
{
public Guid Id { get; set; }
public string Country { get; set; }
public LegalPerson Company { get; set; }
}
public class LegalPerson
{
public string Name { get; set; }
public string IndustrySector { get; set; }
public string Owner { get; set; }
public ContactInfo[] EmailAddresses { get; set; }
public ContactInfo[] PhoneNumbers { get; set; }
}
public class ContactInfo
{
public Guid Id { get; set; }
public string Type { get; set; }
public string Value { get; set; }
}
Now, because of the _embbeded
, I can't do an out-of-the-box serialization with Newtonsoft.Json
, because then Company
will be null
;
I was hoping to see a native hal+json support by Json.NET, but it only has one recommendation to use a custom JsonConverter
.
I started to create a custom one by myself, but feels like "reinventing the wheel" for me.
So, anyone knows a smart way to get out with this?
UPDATE:
The most likely solution is as suggested that you create a custom converter to parse the desired models.
In this case the custom converter would need to be able to read nested paths.
This should provide a simple workaround.
public class NestedJsonPathConverter : JsonConverter {
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer) {
JObject jo = JObject.Load(reader);
var properties = jo.Properties();
object targetObj = existingValue ?? Activator.CreateInstance(objectType);
var resolver = serializer.ContractResolver as DefaultContractResolver;
foreach (PropertyInfo propertyInfo in objectType.GetProperties()
.Where(p => p.CanRead && p.CanWrite)) {
var attributes = propertyInfo.GetCustomAttributes(true).ToArray();
if (attributes.OfType<JsonIgnoreAttribute>().Any())
continue;
var jsonProperty = attributes.OfType<JsonPropertyAttribute>().FirstOrDefault();
var jsonPath = (jsonProperty != null ? jsonProperty.PropertyName : propertyInfo.Name);
if (resolver != null) {
jsonPath = resolver.GetResolvedPropertyName(jsonPath);
}
JToken token = jo.SelectToken(jsonPath) ?? GetTokenCaseInsensitive(properties, jsonPath);
if (token != null && token.Type != JTokenType.Null) {
object value = token.ToObject(propertyInfo.PropertyType, serializer);
propertyInfo.SetValue(targetObj, value, null);
}
}
return targetObj;
}
JToken GetTokenCaseInsensitive(IEnumerable<JProperty> properties, string jsonPath) {
var parts = jsonPath.Split('.');
var property = properties.FirstOrDefault(p =>
string.Equals(p.Name, parts[0], StringComparison.OrdinalIgnoreCase)
);
for (var i = 1; i < parts.Length && property != null && property.Value is JObject; i++) {
var jo = property.Value as JObject;
property = jo.Properties().FirstOrDefault(p =>
string.Equals(p.Name, parts[i], StringComparison.OrdinalIgnoreCase)
);
}
if (property != null && property.Type != JTokenType.Null) {
return property.Value;
}
return null;
}
public override bool CanConvert(Type objectType) {
//Check if any JsonPropertyAttribute has a nested property name {name}.{sub}
return objectType
.GetProperties()
.Any(p =>
p.CanRead
&& p.CanWrite
&& p.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.Any(jp => (jp.PropertyName ?? p.Name).Contains('.'))
);
}
public override bool CanWrite {
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
throw new NotImplementedException();
}
}
The original class structure now does not need to change, with only the properties that require custom paths needing to be decorated with JsonPropertyAttribute
that indicates the path to populate the property.
In this example
public class Customer {
public Guid Id { get; set; }
public string Country { get; set; }
[JsonProperty("_embedded.company")]
public LegalPerson Company { get; set; }
}
public class LegalPerson {
public string Name { get; set; }
public string IndustrySector { get; set; }
public string Owner { get; set; }
[JsonProperty("_embedded.emailAddresses")]
public ContactInfo[] EmailAddresses { get; set; }
[JsonProperty("_embedded.phoneNumbers")]
public ContactInfo[] PhoneNumbers { get; set; }
}
Just include the converter as needed.
var settings = new JsonSerializerSettings {
ContractResolver = new DefaultContractResolver {
NamingStrategy = new CamelCaseNamingStrategy()
}
};
settings.Converters.Add(new NestedJsonPathConverter());
var customer = JsonConvert.DeserializeObject<Customer>(json, settings);
The two important parts of the code are the GetTokenCaseInsensitive
method that searches for the requested token and allows for nested paths that can be case-insensitive.
JToken GetTokenCaseInsensitive(IEnumerable<JProperty> properties, string jsonPath) {
var parts = jsonPath.Split('.');
var property = properties.FirstOrDefault(p =>
string.Equals(p.Name, parts[0], StringComparison.OrdinalIgnoreCase)
);
for (var i = 1; i < parts.Length && property != null && property.Value is JObject; i++) {
var jo = property.Value as JObject;
property = jo.Properties().FirstOrDefault(p =>
string.Equals(p.Name, parts[i], StringComparison.OrdinalIgnoreCase)
);
}
if (property != null && property.Type != JTokenType.Null) {
return property.Value;
}
return null;
}
and the overridden CanConvert
which will check of any properties have nested paths
public override bool CanConvert(Type objectType) {
//Check if any JsonPropertyAttribute has a nested property name {name}.{sub}
return objectType
.GetProperties()
.Any(p =>
p.CanRead
&& p.CanWrite
&& p.GetCustomAttributes(true)
.OfType<JsonPropertyAttribute>()
.Any(jp => (jp.PropertyName ?? p.Name).Contains('.'))
);
}