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
Ответы
Ответ 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 и выше.
Ответ 2
См. https://gist.github.com/LodewijkSioen/5101814
То, что вы искали, было обычным JsonConverter
Ответ 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);
}