Десериализовать hal + json в сложную модель - PullRequest
0 голосов
/ 10 января 2019

У меня есть следующий 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;

Я надеялся увидеть нативную поддержку hal + json от Json.NET, но есть только одна рекомендация использовать пользовательский JsonConverter.

Я начал создавать свой собственный, но мне кажется «изобретать велосипед» заново.

Так, кто-нибудь знает умный способ выйти с этим?

UPDATE:

  • Важно не менять модели / классы. Я могу добавлять атрибуты, но никогда не менять их структуры.

Ответы [ 3 ]

0 голосов
/ 14 января 2019

Возможное решение - использовать пользовательский 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);
0 голосов
/ 14 января 2019

Наиболее вероятное решение заключается в том, что вы предлагаете создать собственный конвертер для анализа нужных моделей.

В этом случае пользовательский конвертер должен иметь возможность считывать вложенные пути.

Это должно обеспечить простой обходной путь.

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('.'))
        );
}
0 голосов
/ 14 января 2019

Объект 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; }
    }
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...