将hal+json反序列化为复杂模型
Posted
技术标签:
【中文标题】将hal+json反序列化为复杂模型【英文标题】:Deserialize hal+json to complex model 【发布时间】:2019-06-05 08:46:03 【问题描述】:我有以下HAL+JSON 示例:
"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": "test2@consoto.com",
"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"
以及以下型号:
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;
现在,由于_embbeded
,我无法使用Newtonsoft.Json
进行开箱即用的序列化,因为那时Company
将是null
;
我希望看到 Json.NET 的 native hal+json support,但它只有一个建议使用自定义 JsonConverter
。
我开始自己创建一个自定义的,但对我来说感觉就像“重新发明***”。
那么,有谁知道解决这个问题的聪明方法?
更新:
重要的是不要更改模型/类。我可以添加属性,但不能更改它的结构。【问题讨论】:
你看过 hal 序列化:npmjs.com/package/hal-serializer 吗? @AhmedBinGamal 我会做,但这是一个节点解决方案,我正在寻找一个 c# 永久解决方案。 你说你不能改变你的模型,扩展或覆盖它们怎么样? 这将是一个问题。我这里的场景是,在生产系统上完全工作,并且数据模式的源更改为 hal+json 响应。我不能去更改模型的结构或扩展/覆盖模型 - 否则,我将需要触及整个系统。我需要一个精确的方法。添加一些属性——我可以在运行时做——或者添加JsonConverter
,或者其他我没有想到的选项。
也许只是使用手动 LINQ to JSON -> 对象版本而不进行任何反序列化?它可能比编写反序列化器更容易
【参考方案1】:
最可能的解决方案是建议您创建一个自定义转换器来解析所需的模型。
在这种情况下,自定义转换器需要能够读取嵌套路径。
这应该提供一个简单的解决方法。
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();
原来的类结构现在不需要改变,只有需要自定义路径的属性需要用JsonPropertyAttribute
修饰,表示填充属性的路径。
在这个例子中
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;
只需根据需要包含转换器即可。
var settings = new JsonSerializerSettings
ContractResolver = new DefaultContractResolver
NamingStrategy = new CamelCaseNamingStrategy()
;
settings.Converters.Add(new NestedJsonPathConverter());
var customer = JsonConvert.DeserializeObject<Customer>(json, settings);
代码的两个重要部分是 GetTokenCaseInsensitive
方法,它搜索请求的令牌并允许不区分大小写的嵌套路径。
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;
以及覆盖的CanConvert
将检查任何属性是否具有嵌套路径
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('.'))
);
【讨论】:
也很有魅力。你可能会看到它在工作here。 @ThiagoLunardi 请注意,在此示例中,您可以排除JsonProperty
属性,因为一旦它们与 json 命名匹配,它们将被自动映射
我赞成您的回答,因为它比@Ivan 的干预更少。如果有更多答案,我会等到赏金到期。非常感谢,现在,赞!【参考方案2】:
可能的解决方案是使用自定义 JsonConverter 但不从头实现所有转换逻辑。
前段时间我发现并更新了JsonPathConverter,它允许使用 JsonProperty 属性的属性路径。例如你的情况
[JsonProperty("_embedded.company")]
public LegalPerson Company get; set;
所以你的带有属性的模型看起来像:
[JsonConverter(typeof(JsonPathConverter))]
public class Customer
[JsonProperty("id")]
public Guid Id get; set;
[JsonProperty("country")]
public string Country get; set;
[JsonProperty("_embedded.company")]
public LegalPerson Company get; set;
[JsonConverter(typeof(JsonPathConverter))]
public class LegalPerson
[JsonProperty("name")]
public string Name get; set;
[JsonProperty("industrySector")]
public string IndustrySector get; set;
[JsonProperty("owner")]
public string Owner get; set;
[JsonProperty("_embedded.emailAddresses")]
public ContactInfo[] EmailAddresses get; set;
[JsonProperty("_embedded.phoneNumbers")]
public ContactInfo[] PhoneNumbers get; set;
public class ContactInfo
[JsonProperty("id")]
public Guid Id get; set;
[JsonProperty("value")]
public string Type get; set;
[JsonProperty("type")]
public string Value get; set;
JsonPathConverter 的代码是这样的。但我相信你可以改进它。
public class JsonPathConverter : JsonConverter
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 resolver)
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);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue,
JsonSerializer serializer)
var jo = JToken.Load(reader);
object targetObj = Activator.CreateInstance(objectType);
foreach (PropertyInfo prop in objectType.GetRuntimeProperties().Where(p => p.CanRead && p.CanWrite))
var attributes = prop.GetCustomAttributes(true).ToArray();
JsonIgnoreAttribute ignoreAttribute = attributes.OfType<JsonIgnoreAttribute>().FirstOrDefault();
if (ignoreAttribute != null)
continue;
JsonPropertyAttribute att = attributes.OfType<JsonPropertyAttribute>().FirstOrDefault();
string jsonPath = att != null ? att.PropertyName : prop.Name;
if (serializer.ContractResolver is DefaultContractResolver resolver)
jsonPath = resolver.GetResolvedPropertyName(jsonPath);
if (!Regex.IsMatch(jsonPath, @"^[a-zA-Z0-9_.-]+$"))
throw new InvalidOperationException(
$"JProperties of JsonPathConverter can have only letters, numbers, underscores, hyphens and dots but name was $jsonPath."); // Array operations not permitted
JToken token = jo.SelectToken(jsonPath);
if (token != null && token.Type != JTokenType.Null)
object value;
var jsonConverterAttr = attributes.OfType<JsonConverterAttribute>().FirstOrDefault();
if (jsonConverterAttr == null)
value = token.ToObject(prop.PropertyType, serializer);
else
var converter = (JsonConverter) Activator.CreateInstance(jsonConverterAttr.ConverterType,
jsonConverterAttr.ConverterParameters);
var r = token.CreateReader();
r.Read();
value = converter.ReadJson(r, prop.PropertyType, prop.GetValue(targetObj),
new JsonSerializer());
prop.SetValue(targetObj, value, null);
return targetObj;
public override bool CanConvert(Type objectType)
// CanConvert is not called when [JsonConverter] attribute is used
return false;
最后你可以像这样使用它:
var json = "*your json string here*";
var customer = JsonConvert.DeserializeObject<Customer>(json);
【讨论】:
这按预期工作,对模型的干预非常少。伟大的!你可以看到工作here。我现在就试试@Nkosi 版本。 @Nkosi 我查看了 JsonPathConverter 的代码,并意识到它可以更好地实现。 here 是。根据我的基准测试,它的运行速度提高了大约 2 倍,并且支持更多的 Newtonsoft Json 属性,例如 JsonIgnore 和 JsonConverter。 我现在可以在没有[JsonConverter(typeof(JsonPathConverter))]
装饰器的情况下运行它吗?
Ivan,你的意思是你改进了@Nkosi 版本?如果是这样,那就太好了!
确实如此,我也做了自己的尝试,它确实更快。感谢@Ivan!【参考方案3】:
company
对象将位于Embedded _embedded
对象之下。
喜欢
class Program
static void Main(string[] args)
string json = "\"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\": \"test2@consoto.com\",\"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\"";
CustomerJson results = JsonConvert.DeserializeObject<CustomerJson>(json);
Customer customer = new Customer()
Id = results.id,
Country = results.country,
Company = new LegalPerson()
EmailAddresses = results._embedded.company._embedded.emailAddresses,
PhoneNumbers = results._embedded.company._embedded.phoneNumbers,
IndustrySector = results._embedded.company.industrySector,
Name = results._embedded.company.name,
Owner = results._embedded.company.owner
;
public class EmbeddedContactInfoJson
public ContactInfo[] emailAddresses get; set;
public ContactInfo[] phoneNumbers get; set;
public class CompanyJson
public string name get; set;
public string industrySector get; set;
public string owner get; set;
public EmbeddedContactInfoJson _embedded get; set;
public EmbeddedLinksJson _links get; set;
public class EmbeddedJson
public CompanyJson company get; set;
public class HrefJson
public string href get; set;
public class EmbeddedLinksJson
public HrefJson self get; set;
public HrefJson phoneNumbers get; set;
public HrefJson addresses get; set;
public class LinksJson
public HrefJson self get; set;
public HrefJson legalPerson get; set;
public HrefJson naturalPerson get; set;
public class CustomerJson
public Guid id get; set;
public string country get; set;
public EmbeddedJson _embedded get; set;
public LinksJson _links get; set;
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;
【讨论】:
无需开箱即用,它甚至可以与自定义JsonConverter
一起使用。但我无法更改模型。这是一个要求。
此外,为每个模型创建一个新的Embedded
类型将是一个丑陋的解决方案。例如。 CompanyEmbbeded
、LegalPersonEmbedded
、OtherModelEmbedded
等等。
我明白这个想法,我明白你的意思。但是有两件事:1)我不能改变模型的结构,所以我必须将值恢复到当前存在的模型。 2) 拥有Self1
、Self2
、Embedded1
、Embedded2
确实是一个丑陋的解决方案。我正在寻找一些精确的东西。例如:创建[Embedded]
属性并处理自定义JsonConverter
。我还不知道怎么解决。
@ThiagoLunardi 我已经更新了代码。1) 使用JsonConverter
使用此模型解析 JSON,然后将结果与现有模型进行映射。 2) 我已将型号名称更改为Self1
、Self2
。您也可以根据需要更改班级名称,这不是问题。如果您有任何问题,请告诉我。
不应该添加Embedded
类,因为我无法更改模型的结构。以上是关于将hal+json反序列化为复杂模型的主要内容,如果未能解决你的问题,请参考以下文章
无法将当前 JSON 数组(例如 [1,2,3])反序列化为具有复杂和嵌套对象的类型