Usando conversores Json.NET para desserializar propriedades

88

Eu tenho uma definição de classe que contém uma propriedade que retorna uma interface.

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

    public ISomething Thing { get; set; }
}

A tentativa de serializar a classe Foo usando Json.NET me dá uma mensagem de erro como, "Não foi possível criar uma instância do tipo 'ISomething'. ISalgo pode ser uma interface ou classe abstrata."

Existe um atributo ou conversor Json.NET que me permita especificar uma Somethingclasse concreta para usar durante a desserialização?

dthrasher
fonte
Eu acredito que você precisa especificar um nome de propriedade que obtém / define ISomething
ram
Eu tenho. Estou usando a abreviação para propriedades implementadas automaticamente introduzidas no C # 3.5. msdn.microsoft.com/en-us/library/bb384054.aspx
dthrasher
4
Não é algo do tipo. Acho que ram está certo, você ainda precisa de um nome de propriedade. Sei que isso não está relacionado ao seu problema, mas seu comentário acima me fez pensar que estava faltando algum novo recurso do .NET que permitia especificar uma propriedade sem um nome.
Sr. Moose

Respostas:

92

Uma das coisas que você pode fazer com Json.NET é:

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

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

O TypeNameHandlingsinalizador adicionará uma $typepropriedade ao JSON, que permite ao Json.NET saber em qual tipo concreto ele precisa para desserializar o objeto. Isso permite que você desserialize um objeto enquanto preenche uma interface ou classe base abstrata.

A desvantagem, entretanto, é que isso é muito específico do Json.NET. O $typeserá um tipo totalmente qualificado, por isso, se você está serialização-lo com a informação tipo ,, as necessidades Deserializer para ser capaz de compreendê-lo bem.

Documentação: Configurações de serialização com Json.NET

Daniel T.
fonte
Interessante. Vou ter que brincar com isso. Boa dica!
dthrasher
2
Para Newtonsoft.Json funciona de forma semelhante, mas a propriedade é "$ type"
Jaap,
Isso foi muito fácil!
Shimmy Weitzhandler
1
Fique atento a possíveis problemas de segurança aqui ao usar TypeNameHandling. Consulte TypeNameHandling cautela em Newtonsoft Json para obter detalhes.
dbc
Eu lutei como um louco com conversores ontem, e isso foi muito melhor e mais compreensível, obrigado !!!
Horothenic
52

Você pode conseguir isso usando a classe JsonConverter. Suponha que você tenha uma classe com uma propriedade de interface;

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

Seu JsonConverter é responsável por serializar e desserializar a propriedade subjacente;

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

Quando você trabalha com uma organização desserializada via Json.Net, o IPerson subjacente da propriedade Owner será do tipo Tycoon.

MrMDavidson
fonte
Muito agradável. Vou ter que dar uma chance ao conversor.
dthrasher
4
A tag "[JsonConverter (typeof (TycoonConverter))]" ainda funcionaria se estivesse em uma lista da interface?
Zwik,
40

Em vez de passar um objeto JsonSerializerSettings personalizado para JsonConvert.SerializeObject () com a opção TypeNameHandling.Objects, conforme mencionado anteriormente, você pode apenas marcar essa propriedade de interface específica com um atributo para que o JSON gerado não seja inflado com propriedades "$ type" em 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
fonte
Brilhante. Obrigado :)
Darren Young
5
Para coleções de interfaces ou classes abstratas, a propriedade é "ItemTypeNameHandling". por exemplo: [JsonProperty (ItemTypeNameHandling = TypeNameHandling.Auto)]
Anthony F
Obrigado por isso!
brudert
24

Na versão mais recente do conversor Newtonsoft Json de terceiros, você pode definir um construtor com um tipo concreto relacionado à propriedade de interface.

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

Contanto que algo implemente ISalgo, isso deve funcionar. Além disso, não coloque um construtor vazio padrão no caso de o conversor JSon tentar usá-lo, você deve forçá-lo a usar o construtor que contém o tipo concreto.

PS. isso também permite que você torne seus setters privados.

SamuelDavis
fonte
6
Isso deveria ser gritado do alto! É verdade que adiciona restrições à implementação concreta, mas é muito mais simples do que as outras abordagens para as situações em que pode ser usado.
Mark Meuer
3
E se tivermos mais de 1 construtor com vários tipos de concreto, ele ainda saberá?
Teoman shipahi de
1
Essa resposta é tão elegante em comparação com todas as bobagens complicadas que você teria de fazer de outra forma. Esta deve ser a resposta aceita. Uma advertência no meu caso, porém, foi que eu tive que adicionar [JsonConstructor] antes do construtor para que funcionasse .... Suspeito que usar isso em apenas UM de seus construtores de concreto resolveria seu problema (de 4 anos) @Teomanshipahi
nacitar sevaht
@nacitarsevaht eu posso voltar atrás e consertar meu problema agora :) de qualquer forma eu nem lembro o que era, mas quando eu olho novamente esta é uma boa solução para alguns casos.
Teoman shipahi
nós também usamos isso, mas eu prefiro converter na maioria dos casos porque acoplar o tipo concreto ao construtor anula o propósito de usar uma interface para a propriedade em primeiro lugar!
gabe
19

Tive o mesmo problema, então criei meu próprio conversor que usa o argumento de tipos conhecidos.

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

Eu defini dois métodos de extensão para desserializar e 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});
    }
}

Você pode definir sua própria maneira de comparar e identificar tipos nos convertidos, eu apenas uso o nome da classe.

Bruno Altinet
fonte
1
Este JsonConverter é ótimo, usei-o, mas enfrentei alguns problemas que resolvi desta forma: - Usando JsonSerializer.CreateDefault () em vez de preencher, porque meu objeto tinha uma hierarquia mais profunda. - Usando reflexão para recuperar o construtor e instanciá-lo no método Create ()
Aurel
3

Normalmente, sempre usei a solução com, TypeNameHandlingconforme sugerido por DanielT, mas nos casos aqui não tive controle sobre o JSON de entrada (e, portanto, não posso garantir que inclua uma $typepropriedade), escrevi um conversor personalizado que apenas permite que você especifique explicitamente o tipo de concreto:

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

Isso apenas usa a implementação do serializador padrão do Json.Net enquanto especifica explicitamente o tipo concreto.

O código-fonte e uma visão geral estão disponíveis nesta postagem do blog .

Steve Greatrex
fonte
1
Esta é uma otima soluçao. Felicidades.
JohnMetta
2

Eu só queria completar o exemplo que @Daniel T. nos mostrou acima:

Se você estiver usando este código para serializar seu objeto:

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

O código para desserializar o json deve ser assim:

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

É assim que um json é conformado ao usar o TypeNameHandlingsinalizador:insira a descrição da imagem aqui

Luis armando
fonte
-5

Eu me perguntei a mesma coisa, mas temo que não possa ser feito.

Vamos ver desta forma. Você entrega ao JSon.net uma string de dados e um tipo para desserializar. O que o JSON.net deve fazer quando atingir esse ISomething? Ele não pode criar um novo tipo de ISomething porque ISomething não é um objeto. Ele também não pode criar um objeto que implemente ISomething, uma vez que não tem ideia de qual dos muitos objetos que podem herdar ISomething ele deve usar. Interfaces são algo que pode ser serializado automaticamente, mas não desserializado automaticamente.

O que eu faria seria substituir ISomething por uma classe base. Usando isso, você poderá obter o efeito que está procurando.

Timothy Baldridge
fonte
1
Eu sei que não vai funcionar "fora da caixa". Mas eu queria saber se havia algum atributo como "[JsonProperty (typeof (SomethingBase))]" que eu pudesse usar para fornecer uma classe concreta.
dthrasher de
Então, por que não usar SomethingBase em vez de ISomething no código acima? Pode-se argumentar que também estamos olhando para isso da maneira errada, pois as interfaces não devem ser usadas na serialização, uma vez que simplesmente definem a "interface" de comunicação com uma determinada classe. Serializar uma interface tecnicamente é um absurdo, assim como serializar uma classe abstrata. Portanto, embora "pudesse ser feito", eu argumentaria que "não deveria ser feito".
Timothy Baldridge
Você já olhou para alguma das classes no namespace Newtonsoft.Json.Serialization? particularmente a classe JsonObjectContract?
johnny de
-9

Aqui está uma referência a um artigo escrito por ScottGu

Com base nisso, escrevi um código que acho que pode 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);
    }
}

E é assim que você o chamaria

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}

Se bem entendi, não acho que você precise especificar uma classe concreta que implemente a interface para serialização JSON.

RAM
fonte
1
Seu exemplo usa o JavaScriptSerializer, uma classe do .NET Framework. Estou usando Json.NET como meu serializador. codeplex.com/Json
dthrasher
3
Não se refere à pergunta original, Json.NET foi explicitamente mencionado lá.
Oliver