我有以下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 的本机 hal+json 支持,但它只有一个使用自定义JsonConverter
的建议。
我开始自己创建一个自定义的,但感觉就像"重新发明轮子"对我来说。
那么,有人知道一个聪明的方法吗?
更新:
- 重要的是不要更改模型/类。我可以添加属性,但永远不会更改其结构。
最可能的解决方案是建议您创建自定义转换器来解析所需的模型。
在这种情况下,自定义转换器需要能够读取嵌套路径。
这应该提供一个简单的解决方法。
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('.'))
);
}
可能的解决方案是使用自定义 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);
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; }
}