Interfaces de fundición para deserialización en JSON.NET


Estoy tratando de configurar un lector que tome objetos JSON de varios sitios web (piense en el raspado de información) y los traduzca a objetos C#. Actualmente estoy usando JSON.NET para el proceso de deserialización. El problema con el que me estoy topando es que no sabe cómo manejar las propiedades de nivel de interfaz en una clase. Así que algo de la naturaleza:

public IThingy Thing

Producirá el error:

No se pudo crear una instancia de tipo IThingy. Type es una interfaz o clase abstracta y no puede ser instanciada.

Es relativamente importante que sea un IThingy en lugar de una Cosita, ya que el código en el que estoy trabajando se considera sensible y las pruebas unitarias son muy importantes. La burla de objetos para scripts de pruebas atómicas no es posible con objetos de pleno derecho como Thingy. Deben ser una interfaz.

He estado estudiando detenidamente JSON.NET ' s documentación desde hace un tiempo, y las preguntas que pude encontrar en este sitio relacionados con esto son todas de hace más de un año. Alguna ayuda?

También, si importa, mi aplicación está escrita en.NET 4.0.

Author: Ray Hayes, 2011-04-25

15 answers

@SamualDavis proporcionó una gran solución en una pregunta relacionada , que resumiré aquí.

Si tiene que deserializar un flujo JSON en una clase concrete que tiene propiedades de interfaz, puede incluir las clases concrete como parámetros a un constructor para la clase! El deserializador de NewtonSoft es lo suficientemente inteligente como para darse cuenta de que necesita usar esas clases concretas para deserializar las propiedades.

Aquí hay un ejemplo:

public class Visit : IVisit
{
    /// <summary>
    /// This constructor is required for the JSON deserializer to be able
    /// to identify concrete classes to use when deserializing the interface properties.
    /// </summary>
    public Visit(MyLocation location, Guest guest)
    {
        Location = location;
        Guest = guest;
    }
    public long VisitId { get; set; }
    public ILocation Location { get;  set; }
    public DateTime VisitDate { get; set; }
    public IGuest Guest { get; set; }
}
 88
Author: Mark Meuer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:02:56

(Copiado de esta pregunta )

En los casos en los que no he tenido control sobre el JSON entrante (y por lo tanto no puedo asegurar que incluya una propiedad type type) he escrito un convertidor personalizado que solo le permite especificar explícitamente el tipo concreto:

public class Model
{
    [JsonConverter(typeof(ConcreteTypeConverter<Something>))]
    public ISomething TheThing { get; set; }
}

Esto solo usa la implementación de serializador predeterminada desde Json.Net especificando explícitamente el tipo de hormigón.

Un resumen está disponible en esta entrada del blog. El código fuente es abajo:

public class ConcreteTypeConverter<TConcrete> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        //assume we can convert to anything for now
        return true;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        //explicitly specify the concrete type we want to create
        return serializer.Deserialize<TConcrete>(reader);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        //use the default serialization - it works fine
        serializer.Serialize(writer, value);
    }
}
 41
Author: Steve Greatrex,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:55:10

Para habilitar la deserialización de múltiples implementaciones de interfaces, puede usar JsonConverter, pero no a través de un atributo:

Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
serializer.Converters.Add(new DTOJsonConverter());
Interfaces.IEntity entity = serializer.Deserialize(jsonReader);

DTOJsonConverter mapea cada interfaz con una implementación concreta:

class DTOJsonConverter : Newtonsoft.Json.JsonConverter
{
    private static readonly string ISCALAR_FULLNAME = typeof(Interfaces.IScalar).FullName;
    private static readonly string IENTITY_FULLNAME = typeof(Interfaces.IEntity).FullName;


    public override bool CanConvert(Type objectType)
    {
        if (objectType.FullName == ISCALAR_FULLNAME
            || objectType.FullName == IENTITY_FULLNAME)
        {
            return true;
        }
        return false;
    }

    public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
    {
        if (objectType.FullName == ISCALAR_FULLNAME)
            return serializer.Deserialize(reader, typeof(DTO.ClientScalar));
        else if (objectType.FullName == IENTITY_FULLNAME)
            return serializer.Deserialize(reader, typeof(DTO.ClientEntity));

        throw new NotSupportedException(string.Format("Type {0} unexpected.", objectType));
    }

    public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}

DTOJsonConverter solo es necesario para el deserializador. El proceso de serialización no cambia. El objeto Json no necesita incrustar nombres de tipos concretos.

This SO post ofrece la misma solución un paso más allá con un genérico JsonConverter.

 34
Author: Eric Boumendil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:18:24

¿Por qué usar un convertidor? Hay una funcionalidad nativa en Newtonsoft.Json para resolver este problema exacto:

Set TypeNameHandling en el JsonSerializerSettings a TypeNameHandling.Auto

JsonConvert.SerializeObject(
        toSerialize,
        new JsonSerializerSettings()
        {
          TypeNameHandling = TypeNameHandling.Auto
        });

Esto pondrá cada tipo en el json, que no se mantiene como una instancia concreta de un tipo, sino como una interfaz o una clase abstracta.

Lo probé, y funciona como un encanto, incluso con listas.

Fuente y una implementación manual alternativa: Código Dentro del Blog

 28
Author: Mafii,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-16 08:36:30

Encontré esto útil. Tú también podrías.

Ejemplo De Uso

public class Parent
{
    [JsonConverter(typeof(InterfaceConverter<IChildModel, ChildModel>))]
    IChildModel Child { get; set; }
}

Convertidor de Creación personalizada

public class InterfaceConverter<TInterface, TConcrete> : CustomCreationConverter<TInterface>
    where TConcrete : TInterface, new()
{
    public override TInterface Create(Type objectType)
    {
        return new TConcrete();
    }
}

Json.NET documentación

 8
Author: smiggleworth,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-21 18:01:37

Dos cosas que podrías intentar:

Implementar un modelo try / parse:

public class Organisation {
  public string Name { get; set; }

  [JsonConverter(typeof(RichDudeConverter))]
  public IPerson Owner { get; set; }
}

public interface IPerson {
  string Name { get; set; }
}

public class Tycoon : IPerson {
  public string Name { get; set; }
}

public class Magnate : IPerson {
  public string Name { get; set; }
  public string IndustryName { get; set; }
}

public class Heir: IPerson {
  public string Name { get; set; }
  public IPerson Benefactor { get; set; }
}

public class RichDudeConverter : JsonConverter
{
  public override bool CanConvert(Type objectType)
  {
    return (objectType == typeof(IPerson));
  }

  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  {
    // pseudo-code
    object richDude = serializer.Deserialize<Heir>(reader);

    if (richDude == null)
    {
        richDude = serializer.Deserialize<Magnate>(reader);
    }

    if (richDude == null)
    {
        richDude = serializer.Deserialize<Tycoon>(reader);
    }

    return richDude;
  }

  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  {
    // Left as an exercise to the reader :)
    throw new NotImplementedException();
  }
}

O, si puede hacerlo en su modelo de objetos, implemente una clase base concreta entre IPerson y sus objetos hoja, y deserialice a ella.

El primero puede potencialmente fallar en tiempo de ejecución, el segundo requiere cambios en su modelo de objetos y homogeneiza la salida al mínimo común denominador.

 5
Author: mcw0933,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-25 18:10:01

Para aquellos que podrían ser curiosos sobre el ConcreteListTypeConverter que fue referenciado por Oliver, aquí está mi intento:

public class ConcreteListTypeConverter<TInterface, TImplementation> : JsonConverter where TImplementation : TInterface 
{
    public override bool CanConvert(Type objectType)
    {
        return true;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var res = serializer.Deserialize<List<TImplementation>>(reader);
        return res.ConvertAll(x => (TInterface) x);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}
 4
Author: Matt M,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-06-10 19:27:28

Supongamos un ajuste de autofac como el siguiente:

public class AutofacContractResolver : DefaultContractResolver
{
    private readonly IContainer _container;

    public AutofacContractResolver(IContainer container)
    {
        _container = container;
    }

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        JsonObjectContract contract = base.CreateObjectContract(objectType);

        // use Autofac to create types that have been registered with it
        if (_container.IsRegistered(objectType))
        {
           contract.DefaultCreator = () => _container.Resolve(objectType);
        }  

        return contract;
    }
}

Entonces, supongamos que su clase es así:

public class TaskController
{
    private readonly ITaskRepository _repository;
    private readonly ILogger _logger;

    public TaskController(ITaskRepository repository, ILogger logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public ITaskRepository Repository
    {
        get { return _repository; }
    }

    public ILogger Logger
    {
        get { return _logger; }
    }
}

Por lo tanto, el uso del solucionador en deserialization podría ser como:

ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<TaskRepository>().As<ITaskRepository>();
builder.RegisterType<TaskController>();
builder.Register(c => new LogService(new DateTime(2000, 12, 12))).As<ILogger>();

IContainer container = builder.Build();

AutofacContractResolver contractResolver = new AutofacContractResolver(container);

string json = @"{
      'Logger': {
        'Level':'Debug'
      }
}";

// ITaskRespository and ILogger constructor parameters are injected by Autofac 
TaskController controller = JsonConvert.DeserializeObject<TaskController>(json, new JsonSerializerSettings
{
    ContractResolver = contractResolver
});

Console.WriteLine(controller.Repository.GetType().Name);

Puedes ver más detalles en http://www.newtonsoft.com/json/help/html/DeserializeWithDependencyInjection.htm

 4
Author: OmG,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-23 15:47:30

Utilice esta clase, para asignar el tipo abstracto al tipo real:

public class AbstractConverter<TReal, TAbstract> : JsonConverter
{
    public override Boolean CanConvert(Type objectType) 
        => objectType == typeof(TAbstract);

    public override Object ReadJson(JsonReader reader, Type type, Object value, JsonSerializer jser) 
        => jser.Deserialize<TReal>(reader);

    public override void WriteJson(JsonWriter writer, Object value, JsonSerializer jser) 
        => jser.Serialize(writer, value);
}

...y cuando deserializar:

        var settings = new JsonSerializerSettings
        {
            Converters = {
                new AbstractConverter<Thing, IThingy>(),
                new AbstractConverter<Thing2, IThingy2>()
            },
        };

        JsonConvert.DeserializeObject(json, type, settings);
 3
Author: Gildor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-22 09:04:08

Por si sirve de algo, terminé teniendo que manejar esto yo mismo en su mayor parte. Cada objeto tiene un método Deserialize(string jsonStream). Algunos fragmentos de él:

JObject parsedJson = this.ParseJson(jsonStream);
object thingyObjectJson = (object)parsedJson["thing"];
this.Thing = new Thingy(Convert.ToString(thingyObjectJson));

En este caso, new Thingy(string) es un constructor que llamará al método Deserialize(string jsonStream) del tipo concreto apropiado. Este esquema continuará yendo hacia abajo y hacia abajo hasta que llegue a los puntos base que json.NET puedo manejarlo.

this.Name = (string)parsedJson["name"];
this.CreatedTime = DateTime.Parse((string)parsedJson["created_time"]);

So y así sucesivamente. Esta configuración me permitió dar json.NET configuraciones que puede manejar sin tener que refactorizar una gran parte de la biblioteca en sí o usar modelos de prueba/análisis difíciles de manejar que habrían atascado toda nuestra biblioteca debido al número de objetos involucrados. También significa que puedo manejar eficazmente cualquier cambio json en un objeto específico, y no necesito preocuparme por todo lo que toque ese objeto. No es de ninguna manera la solución ideal, pero funciona bastante bien desde nuestra unidad y pruebas de integración.

 2
Author: tmesser,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-05-19 14:35:22

Varios años después tuve un problema similar. En mi caso había interfaces muy anidadas y una preferencia por generar las clases concretas en tiempo de ejecución para que funcionara con una clase genérica.

Decidí crear una clase proxy en tiempo de ejecución que envuelve el objeto devuelto por Newtonsoft.

La ventaja de este enfoque es que no requiere una implementación concreta de la clase y puede manejar cualquier profundidad de interfaces anidadas automáticamente. Puedes ver más sobre esto en mi blog .

using Castle.DynamicProxy;
using Newtonsoft.Json.Linq;
using System;
using System.Reflection;

namespace LL.Utilities.Std.Json
{
    public static class JObjectExtension
    {
        private static ProxyGenerator _generator = new ProxyGenerator();

        public static dynamic toProxy(this JObject targetObject, Type interfaceType) 
        {
            return _generator.CreateInterfaceProxyWithoutTarget(interfaceType, new JObjectInterceptor(targetObject));
        }

        public static InterfaceType toProxy<InterfaceType>(this JObject targetObject)
        {

            return toProxy(targetObject, typeof(InterfaceType));
        }
    }

    [Serializable]
    public class JObjectInterceptor : IInterceptor
    {
        private JObject _target;

        public JObjectInterceptor(JObject target)
        {
            _target = target;
        }
        public void Intercept(IInvocation invocation)
        {

            var methodName = invocation.Method.Name;
            if(invocation.Method.IsSpecialName && methodName.StartsWith("get_"))
            {
                var returnType = invocation.Method.ReturnType;
                methodName = methodName.Substring(4);

                if (_target == null || _target[methodName] == null)
                {
                    if (returnType.GetTypeInfo().IsPrimitive || returnType.Equals(typeof(string)))
                    {

                        invocation.ReturnValue = null;
                        return;
                    }

                }

                if (returnType.GetTypeInfo().IsPrimitive || returnType.Equals(typeof(string)))
                {
                    invocation.ReturnValue = _target[methodName].ToObject(returnType);
                }
                else
                {
                    invocation.ReturnValue = ((JObject)_target[methodName]).toProxy(returnType);
                }
            }
            else
            {
                throw new NotImplementedException("Only get accessors are implemented in proxy");
            }

        }
    }



}

Uso:

var jObj = JObject.Parse(input);
InterfaceType proxyObject = jObj.toProxy<InterfaceType>();
 2
Author: Sudsy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-12 11:19:43

Nicholas Westby proporcionó una gran solución en un artículo impresionante .

Si desea deserializar JSON a una de las muchas clases posibles que implementan una interfaz como esa:

public class Person
{
    public IProfession Profession { get; set; }
}

public interface IProfession
{
    string JobTitle { get; }
}

public class Programming : IProfession
{
    public string JobTitle => "Software Developer";
    public string FavoriteLanguage { get; set; }
}

public class Writing : IProfession
{
    public string JobTitle => "Copywriter";
    public string FavoriteWord { get; set; }
}

public class Samples
{
    public static Person GetProgrammer()
    {
        return new Person()
        {
            Profession = new Programming()
            {
                FavoriteLanguage = "C#"
            }
        };
    }
}

Puede usar un convertidor JSON personalizado:

public class ProfessionConverter : JsonConverter
{
    public override bool CanWrite => false;
    public override bool CanRead => true;
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(IProfession);
    }
    public override void WriteJson(JsonWriter writer,
        object value, JsonSerializer serializer)
    {
        throw new InvalidOperationException("Use default serialization.");
    }

    public override object ReadJson(JsonReader reader,
        Type objectType, object existingValue,
        JsonSerializer serializer)
    {
        var jsonObject = JObject.Load(reader);
        var profession = default(IProfession);
        switch (jsonObject["JobTitle"].Value())
        {
            case "Software Developer":
                profession = new Programming();
                break;
            case "Copywriter":
                profession = new Writing();
                break;
        }
        serializer.Populate(jsonObject.CreateReader(), profession);
        return profession;
    }
}

Y usted tendrá que decorar la propiedad "Profesión" con un atributo JsonConverter para hacerle saber a utilizar su convertidor personalizado:

    public class Person
    {
        [JsonConverter(typeof(ProfessionConverter))]
        public IProfession Profession { get; set; }
    }

Y luego, puede emitir su clase con una interfaz:

Person person = JsonConvert.DeserializeObject<Person>(jsonString);
 2
Author: A. Morel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-02-24 11:59:16

Mi solución a esta, que me gusta porque es muy general, es la siguiente:

/// <summary>
/// Automagically convert known interfaces to (specific) concrete classes on deserialisation
/// </summary>
public class WithMocksJsonConverter : JsonConverter
{
    /// <summary>
    /// The interfaces I know how to instantiate mapped to the classes with which I shall instantiate them, as a Dictionary.
    /// </summary>
    private readonly Dictionary<Type,Type> conversions = new Dictionary<Type,Type>() { 
        { typeof(IOne), typeof(MockOne) },
        { typeof(ITwo), typeof(MockTwo) },
        { typeof(IThree), typeof(MockThree) },
        { typeof(IFour), typeof(MockFour) }
    };

    /// <summary>
    /// Can I convert an object of this type?
    /// </summary>
    /// <param name="objectType">The type under consideration</param>
    /// <returns>True if I can convert the type under consideration, else false.</returns>
    public override bool CanConvert(Type objectType)
    {
        return conversions.Keys.Contains(objectType);
    }

    /// <summary>
    /// Attempt to read an object of the specified type from this reader.
    /// </summary>
    /// <param name="reader">The reader from which I read.</param>
    /// <param name="objectType">The type of object I'm trying to read, anticipated to be one I can convert.</param>
    /// <param name="existingValue">The existing value of the object being read.</param>
    /// <param name="serializer">The serializer invoking this request.</param>
    /// <returns>An object of the type into which I convert the specified objectType.</returns>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        try
        {
            return serializer.Deserialize(reader, this.conversions[objectType]);
        }
        catch (Exception)
        {
            throw new NotSupportedException(string.Format("Type {0} unexpected.", objectType));
        }
    }

    /// <summary>
    /// Not yet implemented.
    /// </summary>
    /// <param name="writer">The writer to which I would write.</param>
    /// <param name="value">The value I am attempting to write.</param>
    /// <param name="serializer">the serializer invoking this request.</param>
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

}

Obviamente y trivialmente podría convertirlo en un convertidor aún más general agregando un constructor que tomó un argumento de tipo Dictionary con el que crear instancias de la variable de instancia conversiones.

 1
Author: Simon Brooke,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-21 18:06:35

Ningún objeto jamás ser un IThingy ya que todas las interfaces son abstractas por definición.

El objeto que tienes que se serializó por primera vez era de algún tipo concreto, implementando la interfaz abstract. Es necesario que esta misma clase concrete reviva los datos serializados.

El objeto resultante será entonces de algún tipo que implemente la interfaz abstract que está buscando.

De la documentación se deduce que se puede utilizar

(Thingy)JsonConvert.DeserializeObject(jsonString, typeof(Thingy));

Al deserializar para informar JSON.NET sobre el tipo concreto.

 0
Author: Sean Kinsey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-25 18:01:26

Mi solución se añadió los elementos de interfaz en el constructor.

public class Customer: ICustomer{
     public Customer(Details details){
          Details = details;
     }

     [JsonProperty("Details",NullValueHnadling = NullValueHandling.Ignore)]
     public IDetails Details {get; set;}
}
 0
Author: Jorge Santos Neill,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-18 18:19:21