Deserializar json con campos conocidos y desconocidos

Dado el siguiente resultado json: El resultado json predeterminado tiene un conjunto conocido de campos:

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

Pero puede ampliarse con campos adicionales (en este ejemplo _unknown_field_name_1 y _unknown_field_name_2 ) cuyos nombres no se conocen al solicitar el resultado.

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

Me gustaría que el resultado json se serialice y deserialice desde y hacia una clase con propiedades para los campos conocidos y mapee los campos desconocidos (para los cuales no hay propiedades) a una propiedad (o propiedades múltiples) como un diccionario para que puedan ser accedido y modificado.

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

Creo que necesito una forma de conectarme a un serializador json y hacer el mapeo de los miembros que me faltan (tanto para serializar como deserializar). He estado mirando varias posibilidades:

  • json.net y resolución de contratos personalizados (no se puede saber cómo hacerlo)
  • serializador de contrato de datos (solo se puede anular en serializado, onserializado)
  • serialize a dynamic y haga un mapeo personalizado (esto podría funcionar, pero parece mucho trabajo)
  • dejar que el producto herede de DynamicObject (los serializadores funcionan con reflection y no invocan los métodos trygetmember y trysetmember)

Estoy usando restsharp, pero cualquier serializador puede estar conectado.

Ah, y no puedo cambiar el resultado json, y esto o esto tampoco me ayudó.

Actualización: Esto se parece más a esto: http://geekswithblogs.net/DavidHoerster/archive/2011/07/26/json.net-custom-convertersndasha-quick-tour.aspx

Una opción aún más fácil para abordar este problema sería usar JsonExtensionDataAttribute de JSON .NET

 public class MyClass { // known field public decimal TaxRate { get; set; } // extra fields [JsonExtensionData] private IDictionary _extraStuff; } 

Hay una muestra de esto en el blog del proyecto aquí

ACTUALIZAR Tenga en cuenta que esto requiere JSON .NET v5 versión 5 y superior

Ver https://gist.github.com/LodewijkSioen/5101814

Lo que estabas buscando era un JsonConverter personalizado

Esta es una forma de resolverlo, aunque no me gusta tanto. Lo resolví usando Newton / JSON.Net. Supongo que también podrías usar el JsonConverter para la deserialización.

 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()); 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(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 { {"_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(); } public int id { get; set; } public string name { get; set; } [JsonIgnore] public Dictionary UnknownValues { get; set; } } 

}

Pensé que pondría mi sombrero en el ring ya que tuve un problema similar recientemente. Aquí hay un ejemplo del JSON que quería deserializar:

 { "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" } } } } } } 

Esto es para un sistema donde las anulaciones se aplican en diferentes niveles y se heredan en el árbol. En cualquier caso, el modelo de datos que quería era algo que me permitiera tener una bolsa de propiedades con estas reglas de herencia especiales también suministradas.

Con lo que terminé fue lo siguiente:

 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 _extensionData = new Dictionary(); [JsonIgnore] public readonly Dictionary Values = new Dictionary(); [JsonProperty(".plan", NullValueHandling = NullValueHandling.Ignore)] public Dictionary ByPlan; [JsonProperty(".classAndTier", NullValueHandling = NullValueHandling.Ignore)] public Dictionary ByClassAndTier; [JsonProperty(".bargainingUnit", NullValueHandling = NullValueHandling.Ignore)] public Dictionary 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()); _extensionData.Clear(); } } 

La idea básica es esta:

  1. El PropertyBagModel en deserialización por JSON.NET tiene los campos ByPlan, ByClassAndTier, etc. poblados y también tiene el campo private _extensionData poblado.
  2. Entonces JSON.NET llama al método privado OnDeserialized () y eso moverá los datos de _extensionData a Values ​​según corresponda (o lo dejará caer en el suelo de lo contrario; presumiblemente, podría iniciar sesión si fuera algo que quisiera saber). Luego eliminamos la suciedad adicional de _extensionData para que no consum memoria.
  3. En la serialización, el método OnSerializing recibe llamadas donde movemos cosas a _extensionData para que se guarden.
  4. Cuando finaliza la serialización, se llama a OnSerialized y eliminamos las cosas adicionales de _extensionData.

Podríamos eliminar y volver a crear el _extensionData Dictionary cuando sea necesario, pero no vi un valor real en esto ya que no estoy usando toneladas de estos objetos. Para hacer esto, solo creamos OnSerializing y eliminamos en OnSerialized. En OnDeserializing, en lugar de borrarlo, podríamos liberarlo.

Estaba investigando un problema similar y encontré esta publicación.

Aquí hay una manera de hacerlo usando la reflexión.

Para hacerlo más genérico, uno debe verificar el tipo de la propiedad en lugar de simplemente usar ToString () en propertyInfo.SetValue, a menos que OFC todas las propiedades reales sean cadenas.

Además, los nombres de propiedad en minúsculas no son estándar en C #, pero dado que GetProperty distingue entre mayúsculas y minúsculas, hay algunas otras opciones.

 public class Product { private Type _type; public Product() { fields = new Dictionary(); _type = GetType(); } public string id { get; set; } public string name { get; set; } public Dictionary 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); }