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

128

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

public IThingy Thing

Producirá el error:

No se pudo crear una instancia de tipo IThingy. El tipo es una interfaz o clase abstracta y no se puede instanciar.

Es relativamente importante que sea un Ithingy en lugar de un Thingy, ya que el código en el que estoy trabajando se considera confidencial y las pruebas unitarias son muy importantes. La burla de objetos para scripts de prueba atómica no es posible con objetos completos como Thingy. Deben ser una interfaz.

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

Además, si es importante, mi aplicación está escrita en .NET 4.0.

tmesser
fuente

Respuestas:

115

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

Si tiene que deserializar una secuencia JSON en una clase concreta que tiene propiedades de interfaz, ¡puede incluir las clases concretas como parámetros para un constructor para la clase! El deserializador 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; }
}
Mark Meuer
fuente
15
¿Cómo funcionaría esto con una ICollection? ICollection <IGuest> Invitados {get; set;}
DrSammyD
12
Funciona con ICollection <ConcreteClass>, por lo que funciona ICollection <Guest>. Al igual que para su información, puede poner el atributo [JsonConstructor] en su constructor para que lo use por defecto si tiene múltiples constructores
DrSammyD
66
Estoy atascado con el mismo problema, en mi caso, tengo varias implementaciones de la interfaz (en su ejemplo, la interfaz es ILocation), ¿y qué si hay clases como MyLocation, VIPLocation, OrdinaryLocation? ¿Cómo asignar estos a la propiedad Ubicación? Si solo tiene una implementación como MyLocation, es fácil, pero ¿cómo hacerlo si hay múltiples implementaciones de ILocation?
ATHER
10
Si tiene más de un constructor, puede marcar su constructor especial con el [JsonConstructor]atributo
Dr. Rob Lang
26
Esto no está bien en absoluto. El punto de usar interfaces es usar la inyección de dependencia, pero al hacer esto con un parámetro de tipo objeto requerido por su constructor, arruina totalmente el punto de tener una interfaz como propiedad.
Jérôme MEVEL
57

(Copiado de esta pregunta )

En los casos en que no he tenido control sobre el JSON entrante (y, por lo tanto, no puedo asegurar que incluya una propiedad $ 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 predeterminada del serializador de Json.Net mientras especifica explícitamente el tipo concreto.

Una descripción general está disponible en esta publicación de blog . El código fuente está 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);
    }
}
Steve Greatrex
fuente
11
Realmente me gusta este enfoque y lo apliqué a nuestro propio proyecto. Incluso agregué una ConcreteListTypeConverter<TInterface, TImplementation>para manejar miembros de tipo de clase IList<TInterface>.
Oliver
3
Eso es un gran código. Sin concreteTypeConverterembargo, podría ser mejor tener el código real en la pregunta.
Chris
2
@Oliver - ¿Puedes publicar tu ConcreteListTypeConverter<TInterface, TImplementation>implementación?
Michael
2
¿Y si tienes dos implementadores de ISomething?
bdaniel7
56

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

Establecer TypeNameHandlingen el JsonSerializerSettingsqueTypeNameHandling.Auto

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

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

Asegúrese de estar utilizando la misma configuración para la serialización y deserialización .

Lo probé y funciona de maravilla, incluso con listas.

Resultados de búsqueda Resultado web con enlaces a sitios

⚠️ ADVERTENCIA :

Solo use esto para json de una fuente conocida y confiable. El usuario snipsnipsnip mencionó correctamente que esto es de hecho una vulnerabilidad.

Consulte CA2328 y SCS0028 para obtener más información.


Fuente y una implementación manual alternativa: Code Inside Blog

Mafia
fuente
3
Perfecto, esto me ayudó para un clon profundo rápido y sucio ( stackoverflow.com/questions/78536/deep-cloning-objects )
Compufreak
1
@Shimmy Objects: "Incluya el nombre del tipo .NET al serializar en una estructura de objeto JSON". Automático: incluya el nombre del tipo .NET cuando el tipo del objeto que se está serializando no es el mismo que el tipo declarado. Tenga en cuenta que esto no incluye el objeto serializado raíz de forma predeterminada. Para incluir el nombre de tipo del objeto raíz en JSON, debe especificar un objeto de tipo raíz con SerializeObject (Object, Type, JsonSerializerSettings) o Serialize (JsonWriter, Object, Type). "Fuente: newtonsoft.com/json/help/html/…
Mafii
44
Acabo de probar esto en Deserialización y no funciona. El asunto de esta pregunta de desbordamiento de pila es "Transmitir interfaces para la deserialización en JSON.NET"
Justin Russo el
3
@JustinRusso solo funciona cuando el json ha sido serializado con la misma configuración
Mafii
3
Vota por la solución rápida, si no sucia. Si solo está serializando configuraciones, esto funciona. Es mejor que detener el desarrollo para construir convertidores y ciertamente es mejor que decorar cada propiedad inyectada. serializer.TypeNameHandling = TypeNameHandling.Auto; JsonConvert.DefaultSettings (). TypeNameHandling = TypeNameHandling.Auto;
Sean Anderson
39

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 asigna 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 se requiere para el deserializador. El proceso de serialización no ha cambiado. El objeto Json no necesita incrustar nombres de tipos concretos.

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

Eric Boumendil
fuente
¿No sería la llamada del método WriteJson al serializador? Serializar causar un desbordamiento de la pila, ya que llamar a serializar en el valor que el convertidor serializa provocaría que el método WriteJson del convertidor se vuelva a llamar de forma recursiva?
Triynko
No debería, si el método CanConvert () devuelve un resultado consistente.
Eric Boumendil
3
¿Por qué estás comparando FullNames cuando solo puedes comparar tipos directamente?
Alex Zhukovskiy
Solo comparar tipos también está bien.
Eric Boumendil
23

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

public class AbstractConverter<TReal, TAbstract> : JsonConverter where TReal : TAbstract
{
    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 se deserializa:

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

        JsonConvert.DeserializeObject(json, type, settings);
Gildor
fuente
1
Realmente me gusta una buena respuesta concisa que resuelva mi problema. ¡No necesitas autofac ni nada!
Ben Power
3
Vale la pena poner esto en la declaración de la clase de convertidor: where TReal : TAbstractpara asegurarse de que puede emitir el tipo
Artemious
1
Un lugar más completo donde podría estar where TReal : class, TAbstract, new().
Erik Philips
2
También utilicé este conversor con struct, creo que "donde TReal: TAbstract" es suficiente Gracias a todos.
Gildor
2
¡Oro! Buen camino a seguir.
SwissCoder
12

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 deberá decorar la propiedad "Profesión" con un atributo JsonConverter para que sepa que debe usar 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);
A. Morel
fuente
8

Dos cosas que podrías probar:

Implemente un modelo de prueba / análisis:

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 de hoja, y deserialícela.

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

mcw
fuente
Un modelo de prueba / análisis no es factible debido a la escala con la que tengo que trabajar. Tengo que considerar un alcance de cientos de objetos base con incluso más cientos de objetos de código auxiliar / auxiliar para representar objetos JSON incrustados que suceden mucho. No está fuera de lugar cambiar el modelo de objetos, pero ¿no usar una clase base concreta en las propiedades nos imposibilita para burlarnos de los elementos para la prueba de la unidad? ¿O lo estoy haciendo retroceder de alguna manera?
tmesser
Todavía podría implementar una simulación de IPerson; tenga en cuenta que el tipo de propiedad de la organización. El propietario sigue siendo IPerson. Pero para la deserialización de un objetivo arbitrario, debe devolver un tipo concreto. Si no posee la definición de tipo y no puede definir el conjunto mínimo de propiedades que requerirá su código, entonces su último recurso es algo así como una bolsa de clave / valor. Usando su comentario de ejemplo de Facebook, ¿puede publicar en una respuesta cómo son sus (una o múltiples) implementaciones de ILocation? Eso puede ayudar a avanzar.
mcw
Dado que la principal esperanza es la burla, la interfaz de ubicación es, en realidad, simplemente una fachada para el objeto de hormigón Ubicación. Un ejemplo rápido que acabo de elaborar sería algo como esto ( pastebin.com/mWQtqGnB ) para la interfaz y esto ( pastebin.com/TdJ6cqWV ) para el objeto concreto.
tmesser
Y para ir al siguiente paso, este es un ejemplo de cómo se vería IPage ( pastebin.com/iuGifQXp ) y Page ( pastebin.com/ebqLxzvm ). El problema, por supuesto, es que si bien la deserialización de Page generalmente funcionaría bien, se ahogará cuando llegue a la propiedad ILocation.
tmesser
Bien, entonces, pensando en los objetos que realmente estás raspando y deserializando, ¿es generalmente el caso de que los datos JSON sean consistentes con una única definición de clase concreta? ¿Significa (hipotéticamente) que no encontraría "ubicaciones" con propiedades adicionales que harían que Location no fuera adecuado para usar como tipo concreto para el objeto deserializado? Si es así, debería funcionar atribuir la propiedad ILocation de Page con un "LocationConverter". Si no es así, y es porque los datos JSON no siempre se ajustan a una estructura rígida o consistente (como ILocation), entonces (... continúa)
mcw
8

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();
    }
}

Documentación de Json.NET

Smiggleworth
fuente
1
No es una solución viable. No aborda las listas y conduce a decoradores / anotaciones por aspersión en todas partes.
Sean Anderson
5

Para aquellos que puedan tener curiosidad sobre el ConcreteListTypeConverter al que hizo referencia 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);
    }
}
Matt M
fuente
1
Estoy confundido con lo anulado CanConvert(Type objectType) { return true;}. Parece hacky, ¿cómo es exactamente esto útil? Puedo estar equivocado, pero ¿no es eso como decirle a un luchador más pequeño e inexperto que van a ganar la pelea sin importar el oponente?
Chef_Code
4

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

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

En este caso, el nuevo 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 llegar a los puntos base que json.NET puede manejar.

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

Y así sucesivamente. Esta configuración me permitió dar configuraciones json.NET que puede manejar sin tener que refactorizar una gran parte de la biblioteca o usar modelos difíciles de probar / analizar que habrían empantanado toda nuestra biblioteca debido a la cantidad de objetos involucrados. También significa que puedo manejar efectivamente cualquier cambio de json en un objeto específico, y no tengo que preocuparme por todo lo que toca ese objeto. De ninguna manera es la solución ideal, pero funciona bastante bien desde nuestra unidad y las pruebas de integración.

tmesser
fuente
4

Suponga una configuración de autofac como la 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, suponga 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 la deserialización 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);

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

Dios mio
fuente
Voy a votar esto como la mejor solución. DI ha sido muy utilizado en la actualidad por los desarrolladores web de c #, y esto encaja muy bien como un lugar centralizado para manejar la conversión de tipos por parte del solucionador.
appletwo
3

Ningún objeto será siempre sea un IThingy como interfaces son por definición abstracta.

El objeto que tiene que primero se serializó fue de algún tipo concreto , implementando la interfaz abstracta . Necesita que esta misma clase concreta reviva los datos serializados.

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

De la documentación se deduce que puede usar

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

al deserializar para informar a JSON.NET sobre el tipo concreto.

Sean Kinsey
fuente
Esa es precisamente la publicación de hace más de un año a la que me refería. La única sugerencia importante (escribir convertidores personalizados) no es terriblemente factible con la escala que me veo obligado a considerar. JSON.NET ha cambiado mucho en el año transcurrido. Entiendo perfectamente la distinción entre una clase y una interfaz, pero C # también admite conversiones implícitas de una interfaz a un objeto que implementa la interfaz con respecto a la escritura. Básicamente, estoy preguntando si hay una manera de decirle a JSON.NET qué objeto implementará esta interfaz.
tmesser
Todo estaba allí en la respuesta que te señalé. Asegúrese de que haya una _typepropiedad que indique el tipo de concreto a usar.
Sean Kinsey el
Y dudo mucho que C # admita cualquier tipo de conversión de texto 'implícita' desde una variable declarada como interfaz a un tipo concreto sin ningún tipo de pistas.
Sean Kinsey
A menos que lo lea mal, se suponía que la propiedad _type estaba en el JSON para ser serializada. Eso funciona bien si solo está deserializando lo que ya serializó, pero eso no es lo que está sucediendo aquí. Estoy sacando JSON de varios sitios que no seguirán ese estándar.
tmesser
@YYY: ¿controla tanto la serialización como la deserialización desde la fuente JSON? Debido a que, en última instancia, deberá incrustar el tipo de concreto en el JSON serializado como una pista para usar al deserializar o deberá usar algún tipo de modelo de prueba / análisis que detecte / intente detectar el tipo de concreto en tiempo de ejecución e invocar el deserializador apropiado.
mcw
3

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 al agregar un constructor que tomara un argumento de tipo Diccionario <Tipo, Tipo> con el que instanciar la variable de instancia de conversiones.

Simon Brooke
fuente
3

Varios años después y 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 envuelva 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>();
Jabonoso
fuente
¡Gracias! Esta es la única respuesta que admite correctamente la escritura dinámica (escritura de pato) sin forzar restricciones en el json entrante.
Philip Pittle
No hay problema. Me sorprendió un poco ver que no había nada allí afuera. Se ha movido un poco desde ese ejemplo original, así que decidí compartir el código. github.com/sudsy/JsonDuckTyper . También lo publiqué en nuget como JsonDuckTyper. Si encuentra que desea mejorarlo, solo envíeme un PR y me complacerá hacerlo.
Sudsy
Cuando buscaba una solución en esta área, también me encontré con github.com/ekonbenefits/impromptu-interface . No funciona en mi caso, ya que no es compatible con dotnet core 1.0, pero podría funcionar para usted.
Sudsy
Intenté con Impromptu Interface, pero a Json.Net no le gustó hacer un PopulateObjectproxy generado por Impromptu Interface. Desafortunadamente, dejé de usar Duck Typing: fue más fácil crear un serializador de contratos Json personalizado que utilizó la reflexión para encontrar una implementación existente de la interfaz solicitada y usarla.
Philip Pittle
1

Use este JsonKnownTypes , es una forma muy similar de usar, solo agrega discriminador a json:

[JsonConverter(typeof(JsonKnownTypeConverter<Interface1>))]
[JsonKnownType(typeof(MyClass), "myClass")]
public interface Interface1
{  }
public class MyClass : Interface1
{
    public string Something;
}

Ahora, cuando serialice el objeto en json se agregará "$type"con "myClass"valor y se usará para deserializar

Json

{"Something":"something", "$type":"derived"}
Dmitry
fuente
0

A mi solución se le agregaron los elementos de la interfaz en el constructor.

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

     [JsonProperty("Details",NullValueHnadling = NullValueHandling.Ignore)]
     public IDetails Details {get; set;}
}
Jorge Santos Neill
fuente