Usar convertidores Json.NET para deserializar propiedades

88

Tengo una definición de clase que contiene una propiedad que devuelve una interfaz.

public class Foo
{ 
    public int Number { get; set; }

    public ISomething Thing { get; set; }
}

Intentar serializar la clase Foo usando Json.NET me da un mensaje de error como, "No se pudo crear una instancia del tipo 'ISomething'. ISomething puede ser una interfaz o una clase abstracta".

¿Existe un atributo o convertidor de Json.NET que me permita especificar una Somethingclase concreta para usar durante la deserialización?

dthrasher
fuente
Creo que debe especificar un nombre de propiedad que obtenga / establezca ISomething
ram
Yo tengo. Estoy usando la abreviatura de propiedades implementadas automáticamente introducidas en C # 3.5. msdn.microsoft.com/en-us/library/bb384054.aspx
dthrasher
4
No es algo de ese tipo. Creo que ram tiene razón, todavía necesitas un nombre de propiedad. Sé que esto no está relacionado con su problema, pero su comentario anterior me hizo pensar que me faltaba alguna característica nueva en .NET que le permitía especificar una propiedad sin nombre.
Mr Moose

Respuestas:

92

Una de las cosas que puede hacer con Json.NET es:

var settings = new JsonSerializerSettings();
settings.TypeNameHandling = TypeNameHandling.Objects;

JsonConvert.SerializeObject(entity, Formatting.Indented, settings);

La TypeNameHandlingbandera agregará una $typepropiedad al JSON, lo que le permite a Json.NET saber en qué tipo concreto necesita deserializar el objeto. Esto le permite deserializar un objeto sin dejar de cumplir con una interfaz o clase base abstracta.

Sin embargo, la desventaja es que esto es muy específico de Json.NET. La $typehabrá un tipo totalmente cualificado, por lo que si la serialización con información de tipo ,, las necesidades deserializador para poder entenderlo así.

Documentación: Configuración de serialización con Json.NET

Daniel T.
fuente
Interesante. Tendré que jugar con esto. Buen consejo!
dthrasher
2
Para Newtonsoft.Json funciona de manera similar, pero la propiedad es "$ type"
Jaap
¡Eso fue demasiado fácil!
Shimmy Weitzhandler
1
Tenga cuidado con los posibles problemas de seguridad aquí al usar TypeNameHandling. Consulte la precaución TypeNameHandling en Newtonsoft Json para obtener más detalles.
dbc
Ayer luché como loco con los convertidores, y esto fue mucho mejor y más comprensible, ¡¡¡gracias !!!
Horothenic
52

Puede lograr esto mediante el uso de la clase JsonConverter. Suponga que tiene una clase con una propiedad de interfaz;

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

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

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

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

Su JsonConverter es responsable de serializar y deserializar la propiedad subyacente;

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

  public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  {
    return serializer.Deserialize<Tycoon>(reader);
  }

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

Cuando trabaja con una organización deserializada a través de Json.Net, la IPerson subyacente para la propiedad Owner será de tipo Tycoon.

MrMDavidson
fuente
Muy agradable. Tendré que probar el convertidor.
dthrasher
4
¿Seguiría funcionando la etiqueta "[JsonConverter (typeof (TycoonConverter))]" si estuviera en una lista de la interfaz?
Zwik
40

En lugar de pasar un objeto JsonSerializerSettings personalizado a JsonConvert.SerializeObject () con la opción TypeNameHandling.Objects, como se mencionó anteriormente, puede marcar esa propiedad de interfaz específica con un atributo para que el JSON generado no se infle con propiedades "$ type" en CADA objeto:

public class Foo
{
    public int Number { get; set; }

    // Add "$type" property containing type info of concrete class.
    [JsonProperty( TypeNameHandling = TypeNameHandling.Objects )]
    public ISomething { get; set; }
}
Erhhung
fuente
Brillante. Gracias :)
Darren Young
5
Para colecciones de interfaces o clases abstractas, la propiedad es "ItemTypeNameHandling". por ejemplo: [JsonProperty (ItemTypeNameHandling = TypeNameHandling.Auto)]
Anthony F
¡Gracias por esto!
brudert
23

En la versión más reciente del convertidor Newtonsoft Json de terceros, puede configurar un constructor con un tipo concreto relacionado con la propiedad interconectada.

public class Foo
{ 
    public int Number { get; private set; }

    public ISomething IsSomething { get; private set; }

    public Foo(int number, Something concreteType)
    {
        Number = number;
        IsSomething = concreteType;
    }
}

Siempre que Something implemente ISomething, esto debería funcionar. Además, no coloque un constructor vacío predeterminado en caso de que el convertidor JSon intente usarlo, debe forzarlo a usar el constructor que contiene el tipo concreto.

PD. esto también le permite hacer que sus setters sean privados.

SamuelDavis
fuente
6
¡Esto debería gritarse desde los tejados! Es cierto que agrega restricciones a la implementación concreta, pero es mucho más sencillo que los otros enfoques para aquellas situaciones en las que se puede usar.
Mark Meuer
3
¿Qué pasa si tenemos más de 1 constructor con múltiples tipos de concreto, todavía lo sabrá?
Teoman shipahi
1
Esta respuesta es tan elegante en comparación con todas las tonterías complicadas que tendría que hacer de otra manera. Esta debería ser la respuesta aceptada. Sin embargo, una advertencia en mi caso fue que tuve que agregar [JsonConstructor] antes del constructor para que funcione ... Sospecho que usar esto en solo UNO de sus constructores de concreto resolvería su problema (de 4 años) @Teomanshipahi
nacitar sevaht
@nacitarsevaht Puedo volver atrás y solucionar mi problema ahora :) de todos modos ni siquiera recuerdo cuál fue, pero cuando vuelvo a mirar, esta es una buena solución para ciertos casos.
Teoman shipahi
también usamos esto, pero prefiero convertir en la mayoría de los casos porque acoplar el tipo concreto al constructor frustra el propósito de usar una interfaz para la propiedad en primer lugar.
gabe
19

Tuve el mismo problema, así que se me ocurrió mi propio convertidor que usa argumentos de tipos conocidos.

public class JsonKnownTypeConverter : JsonConverter
{
    public IEnumerable<Type> KnownTypes { get; set; }

    public JsonKnownTypeConverter(IEnumerable<Type> knownTypes)
    {
        KnownTypes = knownTypes;
    }

    protected object Create(Type objectType, JObject jObject)
    {
        if (jObject["$type"] != null)
        {
            string typeName = jObject["$type"].ToString();
            return Activator.CreateInstance(KnownTypes.First(x =>typeName.Contains("."+x.Name+",")));
        }

        throw new InvalidOperationException("No supported type");
    }

    public override bool CanConvert(Type objectType)
    {
        if (KnownTypes == null)
            return false;

        return (objectType.IsInterface || objectType.IsAbstract) && KnownTypes.Any(objectType.IsAssignableFrom);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // Load JObject from stream
        JObject jObject = JObject.Load(reader);
        // Create target object based on JObject
        var target = Create(objectType, jObject);
        // Populate the object properties
        serializer.Populate(jObject.CreateReader(), target);
        return target;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Definí dos métodos de extensión para deserializar y serializar:

public static class AltiJsonSerializer
{
    public static T DeserializeJson<T>(this string jsonString, IEnumerable<Type> knownTypes = null)
    {
        if (string.IsNullOrEmpty(jsonString))
            return default(T);

        return JsonConvert.DeserializeObject<T>(jsonString,
                new JsonSerializerSettings
                {
                    TypeNameHandling = TypeNameHandling.Auto, 
                    Converters = new List<JsonConverter>
                        (
                            new JsonConverter[]
                            {
                                new JsonKnownTypeConverter(knownTypes)
                            }
                        )
                }
            );
    }

    public static string SerializeJson(this object objectToSerialize)
    {
        return JsonConvert.SerializeObject(objectToSerialize, Formatting.Indented,
        new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.Auto});
    }
}

Puede definir su propia forma de comparar e identificar tipos en los convertidos, solo uso el nombre de la clase.

Bruno Altinet
fuente
1
Este JsonConverter es genial, lo usé pero enfrenté un par de problemas que resolví de esta manera: - Usando JsonSerializer.CreateDefault () en lugar de Populate, porque mi objeto tenía una jerarquía más profunda. - Uso de la reflexión para recuperar el constructor e instanciarlo en el método Create ()
Aurel
3

Normalmente, siempre he usado la solución con la TypeNameHandlingsugerencia de DanielT, pero en los casos aquí no he tenido control sobre el JSON entrante (y, por lo tanto, no puedo asegurarme de que incluya una $typepropiedad) .He escrito un convertidor personalizado que solo le permite especificar explícitamente el tipo de hormigón:

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

Esto solo usa la implementación del serializador predeterminada de Json.Net mientras especifica explícitamente el tipo concreto.

El código fuente y una descripción general están disponibles en esta publicación de blog .

Steve Greatrex
fuente
1
Esta es una gran solucion. Salud.
JohnMetta
2

Solo quería completar el ejemplo que @Daniel T. nos mostró arriba:

Si está utilizando este código para serializar su objeto:

var settings = new JsonSerializerSettings();
settings.TypeNameHandling = TypeNameHandling.Objects;
JsonConvert.SerializeObject(entity, Formatting.Indented, settings);

El código para deserializar el json debería verse así:

var settings = new JsonSerializerSettings(); 
settings.TypeNameHandling = TypeNameHandling.Objects;
var entity = JsonConvert.DeserializeObject<EntityType>(json, settings);

Así es como se conforma un json cuando se usa la TypeNameHandlingbandera:ingrese la descripción de la imagen aquí

Luis Armando
fuente
-5

Me he preguntado lo mismo, pero me temo que no se puede hacer.

Veámoslo de esta manera. Le entrega a JSon.net una cadena de datos y un tipo para deserializar. ¿Qué debe hacer JSON.net cuando golpea ese ISomething? No puede crear un nuevo tipo de ISomething porque ISomething no es un objeto. Tampoco puede crear un objeto que implemente ISomething, ya que no tiene idea de cuál de los muchos objetos que pueden heredar ISomething debería usar. Las interfaces son algo que se puede serializar automáticamente, pero no deserializar automáticamente.

Lo que haría sería buscar reemplazar ISomething con una clase base. Con eso, es posible que pueda obtener el efecto que está buscando.

Timothy Baldridge
fuente
1
Me doy cuenta de que no funcionará "fuera de la caja". Pero me preguntaba si había algún atributo como "[JsonProperty (typeof (SomethingBase))]" que pudiera usar para proporcionar una clase concreta.
dthrasher
Entonces, ¿por qué no usar SomethingBase en lugar de ISomething en el código anterior? Se podría argumentar que también estamos viendo esto de manera incorrecta, ya que las interfaces no deberían usarse en la serialización, ya que simplemente definen la "interfaz" de comunicación con una clase determinada. Serializar una interfaz técnicamente no tiene sentido, al igual que serializar una clase abstracta. Entonces, aunque "podría hacerse", yo diría que "no debería hacerse".
Timothy Baldridge
¿Ha mirado alguna de las clases en el espacio de nombres Newtonsoft.Json.Serialization? particularmente la clase JsonObjectContract?
Johnny
-9

Aquí hay una referencia a un artículo escrito por ScottGu

Basado en eso, escribí un código que creo que podría ser útil

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

}

public class School : IEducationalInstitute
{
    private string name;
    #region IEducationalInstitute Members

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    #endregion
}

public class Student 
{
    public IEducationalInstitute LocalSchool { get; set; }

    public int ID { get; set; }
}

public static class JSONHelper
{
    public static string ToJSON(this object obj)
    {
        JavaScriptSerializer serializer = new JavaScriptSerializer();
        return serializer.Serialize(obj);
    }
    public  static string ToJSON(this object obj, int depth)
    {
        JavaScriptSerializer serializer = new JavaScriptSerializer();
        serializer.RecursionLimit = depth;
        return serializer.Serialize(obj);
    }
}

Y así es como lo llamarías

School myFavSchool = new School() { Name = "JFK High School" };
Student sam = new Student()
{
    ID = 1,
    LocalSchool = myFavSchool
};
string jSONstring = sam.ToJSON();

Console.WriteLine(jSONstring);
//Result {"LocalSchool":{"Name":"JFK High School"},"ID":1}

Si lo entiendo correctamente, no creo que deba especificar una clase concreta que implemente la interfaz para la serialización JSON.

RAM
fuente
1
Su muestra usa JavaScriptSerializer, una clase en .NET Framework. Estoy usando Json.NET como mi serializador. codeplex.com/Json
dthrasher
3
No se refiere a la pregunta original, Json.NET se mencionó explícitamente allí.
Oliver