Подтвердить что ты не робот

Deserialize json с известными и неизвестными полями

Учитывая следующий результат json: По умолчанию json result имеет известный набор полей:

{
    "id": "7908",
    "name": "product name"
}

Но может быть расширен с дополнительными полями (в этом примере _unknown_field_name_1 и _unknown_field_name_2), имена которых неизвестны при запросе результата.

{
    "id": "7908",
    "name": "product name",
    "_unknown_field_name_1": "some value",
    "_unknown_field_name_2": "some value"
}

Я хотел бы, чтобы результат json был сериализован и десериализован в класс и с классом со свойствами для известных полей и сопоставил неизвестные поля (для которых нет свойств) к свойству (или нескольким свойствам), как словарь, поэтому они могут быть доступны и изменены.

public class Product
{
    public string id { get; set; }
    public string name { get; set; }
    public Dictionary<string, string> fields { get; set; }
}

Я думаю, мне нужен способ подключиться к сериализатору json и сделать сопоставление для отсутствующих элементов (как для сериализации, так и для десериализации). Я рассматривал различные возможности:

  • json.net и пользовательские разрешители контрактов (не могу понять, как это сделать)
  • datacontract serializer (может только переопределять onserialized, onserializing)
  • сериализуем динамику и выполняем настраиваемое сопоставление (это может работать, но, похоже, много работы)
  • пусть продукт наследуется от DynamicObject (сериализаторы работают с отражением и не вызывают методы trygetmember и trysetmember)

Я использую restsharp, но любой сериализатор может быть подключен.

О, и я не могу изменить результат json, а this или не помог мне либо.

Update: Это больше похоже на: http://geekswithblogs.net/DavidHoerster/archive/2011/07/26/json.net-custom-convertersndasha-quick-tour.aspx

4b9b3361

Ответ 1

Еще более простой способ решения этой проблемы - использовать JsonExtensionDataAttribute из JSON.NET.

public class MyClass
{
   // known field
   public decimal TaxRate { get; set; }

   // extra fields
   [JsonExtensionData]
   private IDictionary<string, JToken> _extraStuff;
}

Вот пример этого в блоге проекта здесь

ОБНОВЛЕНИЕ Обратите внимание, что для этого требуется JSON.NET v5 версии 5 и выше.

Ответ 3

Это способ, которым вы могли бы решить это, хотя мне это не очень нравится. Я решил это с помощью Newton/JSON.Net. Я полагаю, вы также можете использовать JsonConverter для десериализации.

private const string Json = "{\"id\":7908,\"name\":\"product name\",\"_unknown_field_name_1\":\"some value\",\"_unknown_field_name_2\":\"some value\"}";

    [TestMethod]
    public void TestDeserializeUnknownMembers()
    {
        var @object = JObject.Parse(Json);

        var serializer = new Newtonsoft.Json.JsonSerializer();
        serializer.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Error;
        serializer.Error += (sender, eventArgs) =>
            {
                var contract = eventArgs.CurrentObject as Contract ?? new Contract();
                contract.UnknownValues.Add(eventArgs.ErrorContext.Member.ToString(), @object[eventArgs.ErrorContext.Member.ToString()].Value<string>());
                eventArgs.ErrorContext.Handled = true;
            };

        using (MemoryStream memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(Json)))
        using (StreamReader streamReader = new StreamReader(memoryStream))
        using (JsonReader jsonReader = new JsonTextReader(streamReader))
        {
            var result = serializer.Deserialize<Contract>(jsonReader);
            Assert.IsTrue(result.UnknownValues.ContainsKey("_unknown_field_name_1"));
            Assert.IsTrue(result.UnknownValues.ContainsKey("_unknown_field_name_2"));
        }
    }

    [TestMethod]
    public void TestSerializeUnknownMembers()
    {
        var deserializedObject = new Contract
        {
            id = 7908,
            name = "product name",
            UnknownValues = new Dictionary<string, string>
        {
            {"_unknown_field_name_1", "some value"},
            {"_unknown_field_name_2", "some value"}
        }
        };

        var json = JsonConvert.SerializeObject(deserializedObject, new DictionaryConverter());
        Console.WriteLine(Json);
        Console.WriteLine(json);
        Assert.AreEqual(Json, json);
    }
}

class DictionaryConverter : JsonConverter
{
    public DictionaryConverter()
    {

    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Contract);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var contract = value as Contract;
        var json = JsonConvert.SerializeObject(value);
        var dictArray = String.Join(",", contract.UnknownValues.Select(pair => "\"" + pair.Key + "\":\"" + pair.Value + "\""));

        json = json.Substring(0, json.Length - 1) + "," + dictArray + "}";
        writer.WriteRaw(json);
    }
}

class Contract
{
    public Contract()
    {
        UnknownValues = new Dictionary<string, string>();
    }

    public int id { get; set; }
    public string name { get; set; }

    [JsonIgnore]
    public Dictionary<string, string> UnknownValues { get; set; }
}
}

Ответ 4

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

{
    "agencyId": "agency1",
    "overrides": {
        "assumption.discount.rates": "value: 0.07",
        ".plan": {
            "plan1": {
                "assumption.payroll.growth": "value: 0.03",
                "provision.eeContrib.rate": "value: 0.35"
            },
            "plan2": {
                ".classAndTier": {
                    "misc:tier1": {
                        "provision.eeContrib.rate": "value: 0.4"
                    },
                    "misc:tier2": {
                        "provision.eeContrib.rate": "value: 0.375"
                    }
                }
            }
        }
    }
}

Это для системы, где переопределения применяются на разных уровнях и наследуются по дереву. В любом случае, модель данных, которую я хотел, была чем-то, что позволило бы мне иметь мешок с этими специальными правилами наследования.

В результате я получил следующее:

public class TestDataModel
{
    public string AgencyId;
    public int Years;
    public PropertyBagModel Overrides;
}

public class ParticipantFilterModel
{
    public string[] ClassAndTier;
    public string[] BargainingUnit;
    public string[] Department;
}

public class PropertyBagModel
{
    [JsonExtensionData]
    private readonly Dictionary<string, JToken> _extensionData = new Dictionary<string, JToken>();

    [JsonIgnore]
    public readonly Dictionary<string, string> Values = new Dictionary<string, string>();

    [JsonProperty(".plan", NullValueHandling = NullValueHandling.Ignore)]
    public Dictionary<string, PropertyBagModel> ByPlan;

    [JsonProperty(".classAndTier", NullValueHandling = NullValueHandling.Ignore)]
    public Dictionary<string, PropertyBagModel> ByClassAndTier;

    [JsonProperty(".bargainingUnit", NullValueHandling = NullValueHandling.Ignore)]
    public Dictionary<string, PropertyBagModel> ByBarginingUnit;

    [OnSerializing]
    private void OnSerializing(StreamingContext context)
    {
        foreach (var kvp in Values)
            _extensionData.Add(kvp.Key, kvp.Value);
    }

    [OnSerialized]
    private void OnSerialized(StreamingContext context)
    {
        _extensionData.Clear();
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        Values.Clear();
        foreach (var kvp in _extensionData.Where(x => x.Value.Type == JTokenType.String))
            Values.Add(kvp.Key, kvp.Value.Value<string>());
        _extensionData.Clear();
    }
}

Основная идея такова:

  • PropertyBagModel при десериализации JSON.NET имеет заполненные поля ByPlan, ByClassAndTier и т.д., а также заполняется поле private _extensionData.
  • Затем JSON.NET вызывает частный метод OnDeserialized(), который будет перемещать данные из _extensionData в значения, если это необходимо (или оставить его на пол в противном случае - возможно, вы могли бы зарегистрировать это, если бы это было то, что вы хотели знать). Затем мы удаляем дополнительный gunk из _extensionData, чтобы он не потреблял память.
  • При сериализации метод OnSerializing получает вызовы, где мы перемещаем материал в _extensionData, чтобы он сохранялся.
  • Когда сериализация завершена, вызывается OnSerialized и мы удаляем лишний материал из _extensionData.

Мы могли бы дополнительно удалить и воссоздать словарь _extensionData, когда это необходимо, но я не видел в этом реальной ценности, так как я не использую тонны этих объектов. Для этого мы просто создадим OnSerializing и удалим OnSerialized. В OnDeserializing вместо очистки мы можем освободить его.

Ответ 5

Я искал похожую проблему и нашел этот пост.

Вот способ сделать это с помощью отражения.

Чтобы сделать его более общим, следует проверять тип свойства вместо простого использования ToString() в propertyInfo.SetValue, если только в OFC все действительные свойства не являются строками.

Кроме того, имена свойств в нижнем регистре не являются стандартными в С#, но, учитывая, что GetProperty чувствителен к регистру, есть несколько других вариантов.

public class Product
{
    private Type _type;

    public Product()
    {
        fields = new Dictionary<string, object>();
        _type = GetType();
    }

    public string id { get; set; }
    public string name { get; set; }
    public Dictionary<string, object> fields { get; set; }

    public void SetProperty(string key, object value)
    {
        var propertyInfo = _type.GetProperty(key);
        if (null == propertyInfo)
        {
            fields.Add(key,value);
            return;
        }
        propertyInfo.SetValue(this, value.ToString());
    }
}
...
private const string JsonTest = "{\"id\":7908,\"name\":\"product name\",\"_unknown_field_name_1\":\"some value\",\"_unknown_field_name_2\":\"some value\"}";

var product = new Product();
var data = JObject.Parse(JsonTest);
foreach (var item in data)
{
    product.SetProperty(item.Key, item.Value);
}