Interfacce di casting per la deserializzazione in JSON.NET


129

Sto cercando di impostare un lettore che accetti oggetti JSON da vari siti Web (pensa allo scraping delle informazioni) e li traduce in oggetti C #. Attualmente sto utilizzando JSON.NET per il processo di deserializzazione. Il problema che sto incontrando è che non sa come gestire le proprietà a livello di interfaccia in una classe. Quindi qualcosa della natura:

public IThingy Thing

Produrrà l'errore:

Impossibile creare un'istanza di tipo IThingy. Il tipo è un'interfaccia o una classe astratta e non può essere istanziato.

È relativamente importante che sia un IThingy anziché un Thingy poiché il codice su cui sto lavorando è considerato sensibile e il test di unità è molto importante. Il deridere di oggetti per script di test atomici non è possibile con oggetti a tutti gli effetti come Thingy. Devono essere un'interfaccia.

Sto esaminando attentamente la documentazione di JSON.NET da un po 'di tempo e le domande che ho potuto trovare su questo sito relative a questo sono tutte di oltre un anno fa. Qualsiasi aiuto?

Inoltre, se è importante, la mia app è scritta in .NET 4.0.


Risposte:


115

@SamualDavis ha fornito un'ottima soluzione in una domanda correlata , che riassumerò qui.

Se devi deserializzare un flusso JSON in una classe concreta che ha proprietà di interfaccia, puoi includere le classi concrete come parametri di un costruttore per la classe! Il deserializzatore NewtonSoft è abbastanza intelligente da capire che ha bisogno di usare quelle classi concrete per deserializzare le proprietà.

Ecco un esempio:

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

15
Come funzionerebbe con una ICollection? ICollection <IGuest> Guests {get; set;}
DrSammyD

12
Funziona con ICollection <ConcreteClass>, quindi ICollection <Guest> funziona. Proprio come FYI, puoi mettere l'attributo [JsonConstructor] sul tuo costruttore in modo che lo utilizzi per impostazione predefinita se ti capita di avere più costruttori
DrSammyD

6
Sono bloccato allo stesso problema, nel mio caso ho diverse implementazioni dell'interfaccia (nel tuo esempio l'interfaccia è ILocation) quindi cosa succede se ci sono classi come MyLocation, VIPLocation, OrdinaryLocation. Come mapparli alla proprietà Location? Se hai solo un'implementazione come MyLocation è facile, ma come farlo se ci sono più implementazioni di ILocation?
ATHER

10
Se hai più di un costruttore, puoi contrassegnare il tuo costruttore speciale con l' [JsonConstructor]attributo.
Dr Rob Lang

26
Questo non va affatto bene. Lo scopo dell'utilizzo delle interfacce è utilizzare l'inserimento delle dipendenze, ma facendo ciò con un parametro di tipo oggetto richiesto dal tuo costruttore, si rovina completamente il punto di avere un'interfaccia come proprietà.
Jérôme MEVEL

57

(Copiato da questa domanda )

Nei casi in cui non ho avuto il controllo sul JSON in arrivo (e quindi non posso garantire che includa una proprietà $ type), ho scritto un convertitore personalizzato che ti consente solo di specificare esplicitamente il tipo concreto:

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

Questo utilizza solo l'implementazione del serializzatore predefinito da Json.Net mentre specifica esplicitamente il tipo concreto.

Una panoramica è disponibile in questo post del blog . Il codice sorgente è di seguito:

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

11
Mi piace molto questo approccio e l'ho applicato al nostro progetto. Ho anche aggiunto un ConcreteListTypeConverter<TInterface, TImplementation>per gestire i membri della classe di tipo IList<TInterface>.
Oliver

3
È un bel pezzo di codice. concreteTypeConverterTuttavia, potrebbe essere più bello avere il codice effettivo per la domanda.
Chris,

2
@Oliver - Puoi pubblicare la tua ConcreteListTypeConverter<TInterface, TImplementation>implementazione?
Michael

2
E se hai due implementatori di ISomething?
bdaniel7

56

Perché usare un convertitore? C'è una funzionalità nativa Newtonsoft.Jsonper risolvere questo problema esatto:

Situato TypeNameHandlingnella JsonSerializerSettingsaTypeNameHandling.Auto

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

Questo inserirà ogni tipo nel json, che non è tenuto come un'istanza concreta di un tipo ma come un'interfaccia o una classe astratta.

Assicurati di utilizzare le stesse impostazioni per la serializzazione e la deserializzazione .

L'ho provato e funziona a meraviglia, anche con le liste.

Risultati della ricerca Risultato web con collegamenti al sito

⚠️ ATTENZIONE :

Usalo solo per json da una fonte conosciuta e attendibile. L'utente snipsnipsnip ha detto correttamente che questa è davvero una vulnerabilità.

Vedere CA2328 e SCS0028 per ulteriori informazioni.


Fonte e un'implementazione manuale alternativa: Code Inside Blog


3
Perfetto, questo mi ha aiutato per una profonda Clonazione veloce e sporco ( stackoverflow.com/questions/78536/deep-cloning-objects )
Compufreak

1
@Shimmy Objects: "Includere il nome del tipo .NET durante la serializzazione in una struttura di oggetti JSON." Auto: include il nome del tipo .NET quando il tipo di oggetto serializzato non è lo stesso del tipo dichiarato. Nota che questo non include l'oggetto serializzato di root per impostazione predefinita. Per includere il nome del tipo di oggetto radice in JSON, è necessario specificare un oggetto di tipo radice con SerializeObject (Object, Type, JsonSerializerSettings) o Serialize (JsonWriter, Object, Type). "Fonte: newtonsoft.com/json/help/html/…
Mafii

4
L'ho appena provato con la deserializzazione e non funziona. La riga dell'oggetto di questa domanda di Stack Overflow è "Interfacce di trasmissione per la deserializzazione in JSON.NET"
Justin Russo,

3
@JustinRusso funziona solo quando il json è stato serializzato con la stessa impostazione
Mafii

3
Vota per la soluzione rapida, se non sporca. Se stai solo serializzando le configurazioni, funziona. È meglio fermare lo sviluppo per costruire convertitori e sicuramente batte decorare ogni proprietà iniettata. serializer.TypeNameHandling = TypeNameHandling.Auto; JsonConvert.DefaultSettings (). TypeNameHandling = TypeNameHandling.Auto;
Sean Anderson

39

Per abilitare la deserializzazione di più implementazioni di interfacce, è possibile utilizzare JsonConverter, ma non tramite un attributo:

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

DTOJsonConverter mappa ogni interfaccia con un'implementazione 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 è richiesto solo per il deserializzatore. Il processo di serializzazione rimane invariato. L'oggetto Json non ha bisogno di incorporare nomi di tipi concreti.

Questo post SO offre la stessa soluzione un ulteriore passo avanti con un JsonConverter generico.


La chiamata del metodo WriteJson a serializer.Serialize non causerebbe un overflow dello stack, poiché la chiamata di serialize sul valore serializzato dal convertitore causerebbe la chiamata ricorsiva del metodo WriteJson del convertitore?
Triynko

Non dovrebbe, se il metodo CanConvert () restituisce un risultato coerente.
Eric Boumendil

3
Perché stai confrontando FullNamei messaggi quando puoi semplicemente confrontare i tipi direttamente?
Alex Zhukovskiy

Anche solo confrontare i tipi va bene.
Eric Boumendil

23

Usa questa classe per mappare il tipo astratto al tipo reale:

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

... e quando deserializza:

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

        JsonConvert.DeserializeObject(json, type, settings);

1
Mi piace molto una bella risposta concisa che risolva il mio problema. Non c'è bisogno di autofac o altro!
Ben Power

3
Vale la pena metterlo nella dichiarazione della classe del convertitore: where TReal : TAbstractper assicurarsi che possa eseguire il cast nel tipo
Artemious

1
Un più completo dove potrebbe essere where TReal : class, TAbstract, new().
Erik Philips

2
Ho usato questo convertitore anche con struct, credo che "where TReal: TAbstract" sia sufficiente. Ringrazia tutti.
Gildor

2
Oro! Ottima strada da percorrere.
SwissCoder

12

Nicholas Westby ha fornito un'ottima soluzione in un fantastico articolo .

Se vuoi deserializzare JSON in una delle tante classi possibili che implementano un'interfaccia come questa:

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

Puoi utilizzare un convertitore JSON personalizzato:

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

E dovrai decorare la proprietà "Profession" con un attributo JsonConverter per fargli sapere di utilizzare il tuo convertitore personalizzato:

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

E poi, puoi trasmettere la tua classe con un'interfaccia:

Person person = JsonConvert.DeserializeObject<Person>(jsonString);

8

Due cose che potresti provare:

Implementa un modello di prova / analisi:

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

Oppure, se è possibile farlo nel modello a oggetti, implementare una classe base concreta tra IPerson e gli oggetti foglia e deserializzarla.

Il primo può potenzialmente fallire in fase di esecuzione, il secondo richiede modifiche al modello a oggetti e omogeneizza l'output al minimo comune denominatore.


Un modello try / parse non è fattibile a causa della scala con cui devo lavorare. Devo considerare un ambito di centinaia di oggetti di base con ancora più centinaia di oggetti stub / helper per rappresentare oggetti JSON incorporati che accadono spesso. Non è escluso cambiare il modello a oggetti, ma l'uso di una classe base concreta nelle proprietà non ci renderebbe incapaci di deridere gli elementi per i test unitari? O lo sto facendo tornare indietro in qualche modo?
tmesser

È ancora possibile implementare una simulazione di IPerson: si noti che il tipo di proprietà Organisation.Owner è ancora IPerson. Ma per la deserializzazione di un target arbitrario devi restituire un tipo concreto. Se non possiedi la definizione del tipo e non puoi definire il set minimo di proprietà che il tuo codice richiederà, l'ultima risorsa è qualcosa come una borsa chiave / valore. Usando il tuo commento di esempio su Facebook, puoi pubblicare in una risposta come appaiono le tue (una o più) implementazioni di ILocation? Questo può aiutare a far progredire le cose.
mcw

Dal momento che la speranza principale è beffarda, l'interfaccia ILocation è, in realtà, solo una facciata per l'oggetto concreto Location. Un rapido esempio che ho appena elaborato potrebbe essere qualcosa come questo ( pastebin.com/mWQtqGnB ) per l'interfaccia e questo ( pastebin.com/TdJ6cqWV ) per l'oggetto concreto.
tmesser

E per andare al passaggio successivo, questo è un esempio di come apparirebbe IPage ( pastebin.com/iuGifQXp ) e Page ( pastebin.com/ebqLxzvm ). Il problema, ovviamente, è che mentre la deserializzazione di Page generalmente funzionerebbe bene, si strozzerà quando arriva alla proprietà ILocation.
tmesser

Ok, quindi pensando agli oggetti che stai effettivamente raschiando e deserializzando - è generalmente il caso che i dati JSON siano coerenti con una singola definizione di classe concreta? Significa (ipoteticamente) che non incontreresti "posizioni" con proprietà aggiuntive che renderebbero Location inadatta da utilizzare come tipo concreto per l'oggetto deserializzato? In tal caso, l'attribuzione della proprietà ILocation di Page con un "LocationConverter" dovrebbe funzionare. In caso contrario, ed è perché i dati JSON non sono sempre conformi a una struttura rigida o coerente (come ILocation), quindi (... continua)
mcw

8

L'ho trovato utile. Potresti farlo anche tu.

Utilizzo di esempio

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

Convertitore di creazione personalizzato

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

Documentazione Json.NET


1
Non è una soluzione praticabile. Non si occupa di elenchi e porta a spargere decoratori / annotazioni ovunque.
Sean Anderson

5

Per coloro che potrebbero essere curiosi del ConcreteListTypeConverter a cui ha fatto riferimento Oliver, ecco il mio tentativo:

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

1
Sono confuso con l'override CanConvert(Type objectType) { return true;}. Sembra hacky, in che modo esattamente è utile? Potrei sbagliarmi, ma non è come dire a un combattente inesperto più piccolo che vincerà il combattimento indipendentemente dall'avversario?
Chef_Code

4

Per quello che vale, ho finito per doverlo gestire da solo per la maggior parte. Ogni oggetto ha un metodo Deserialize (string jsonStream) . Alcuni frammenti di esso:

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

In questo caso, new Thingy (string) è un costruttore che chiamerà il metodo Deserialize (string jsonStream) del tipo concreto appropriato. Questo schema continuerà ad andare verso il basso e verso il basso fino a raggiungere i punti base che json.NET può semplicemente gestire.

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

Così via. Questa configurazione mi ha permesso di fornire configurazioni json.NET che può gestire senza dover rifattorizzare gran parte della libreria stessa o utilizzare modelli di prova / analisi ingombranti che avrebbero impantanato l'intera libreria a causa del numero di oggetti coinvolti. Significa anche che posso gestire efficacemente qualsiasi modifica JSON su un oggetto specifico e non devo preoccuparmi di tutto ciò che tocca l'oggetto. Non è affatto la soluzione ideale, ma funziona abbastanza bene dal nostro test di unità e integrazione.


4

Supponiamo un'impostazione di autofac come la seguente:

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

Quindi, supponiamo che la tua classe sia così:

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

Pertanto, l'utilizzo del resolver nella deserializzazione potrebbe essere come:

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

Puoi vedere maggiori dettagli in http://www.newtonsoft.com/json/help/html/DeserializeWithDependencyInjection.htm


Voterò a favore di questa come la migliore soluzione. DI è stato ampiamente utilizzato in questi giorni dagli sviluppatori web c #, e questo si adatta bene come luogo centralizzato per gestire la conversione di tipi da parte del resolver.
appletwo

3

Nessun oggetto sarà mai essere un IThingy come interfacce sono tutti astratto per definizione.

L'oggetto che hai che è stato serializzato per la prima volta era di un tipo concreto , che implementava l' interfaccia astratta . È necessario che questa stessa classe concreta faccia rivivere i dati serializzati.

L'oggetto risultante sarà quindi di un tipo che implementa l' interfaccia astratta che stai cercando.

Dalla documentazione segue che puoi usare

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

durante la deserializzazione per informare JSON.NET sul tipo di calcestruzzo.


Questo è precisamente il post di oltre un anno fa a cui mi riferivo. L'unico suggerimento importante (scrivere convertitori personalizzati) non è particolarmente fattibile con la scala che sono costretto a considerare. JSON.NET è cambiato molto nel corso dell'anno. Capisco perfettamente la distinzione tra una classe e un'interfaccia, ma C # supporta anche conversioni implicite da un'interfaccia a un oggetto che implementa l'interfaccia per quanto riguarda la digitazione. Sto essenzialmente chiedendo se esiste un modo per dire a JSON.NET quale oggetto implementerà questa interfaccia.
tmesser

Era tutto lì nella risposta che ti ho indicato. Assicurati che ci sia una _typeproprietà che segnali il tipo di cemento da utilizzare.
Sean Kinsey

E dubito fortemente che C # supporti qualsiasi tipo di typecasting "implicito" da una variabile dichiarata come interfaccia a un tipo concreto senza alcun tipo di suggerimento.
Sean Kinsey

A meno che non abbia letto male, la proprietà _type doveva essere nel JSON per essere serializzata. Funziona bene se stai deserializzando solo ciò che hai già serializzato, ma non è quello che sta succedendo qui. Sto estraendo JSON da un numero di siti che non seguiranno quello standard.
tmesser

@YYY - Controlli sia la serializzazione che la deserializzazione dal JSON di origine? Perché alla fine dovrai incorporare il tipo concreto nel JSON serializzato come suggerimento da utilizzare durante la deserializzazione o dovrai utilizzare una sorta di modello di prova / analisi che rileva / tenta di rilevare il tipo concreto in fase di esecuzione e invoca il deserializzatore appropriato.
mcw

3

La mia soluzione a questo, che mi piace perché è piacevolmente generale, è la seguente:

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

}

Puoi ovviamente e banalmente convertirlo in un convertitore ancora più generale aggiungendo un costruttore che ha un argomento di tipo Dictionary <Type, Type> con cui istanziare la variabile di istanza delle conversioni.


3

Diversi anni dopo e ho avuto un problema simile. Nel mio caso c'erano interfacce fortemente annidate e una preferenza per la generazione di classi concrete in fase di esecuzione in modo che funzionasse con una classe generica.

Ho deciso di creare una classe proxy in fase di esecuzione che avvolge l'oggetto restituito da Newtonsoft.

Il vantaggio di questo approccio è che non richiede un'implementazione concreta della classe e può gestire automaticamente qualsiasi profondità di interfacce annidate. Puoi vedere di più al riguardo sul mio 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>();

Grazie! Questa è l'unica risposta che supporta correttamente la digitazione dinamica (dattilografia anatra) senza forzare restrizioni sul json in arrivo.
Philip Pittle,

Nessun problema. Sono rimasto un po 'sorpreso di vedere che non c'era niente là fuori. È passato un po 'da quell'esempio originale, quindi ho deciso di condividere il codice. github.com/sudsy/JsonDuckTyper . L'ho anche pubblicato su nuget come JsonDuckTyper. Se trovi di volerlo migliorare, inviami un PR e sarò felice di accontentarti.
Sudsy

Quando stavo cercando una soluzione in quest'area mi sono imbattuto anche in github.com/ekonbenefits/impromptu-interface . Nel mio caso non funziona in quanto non supporta dotnet core 1.0 ma potrebbe funzionare per te.
Sudsy

Ho provato con Impromptu Interface, ma Json.Net non era contento di fare un PopulateObjectproxy generato da Impromptu Interface. Sfortunatamente ho rinunciato a Duck Typing: era semplicemente più semplice creare un serializzatore di contratti Json personalizzato che utilizzava la reflection per trovare un'implementazione esistente dell'interfaccia richiesta e utilizzarla.
Philip Pittle,

1

Usa questo JsonKnownTypes , è un modo molto simile da usare, aggiunge semplicemente il discriminatore a json:

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

Ora, quando si serializza oggetto in JSON sarà aggiunge "$type"con "myClass"il valore e sarà utilizzato per deserializzare

jSON:

{"Something":"something", "$type":"derived"}

0

La mia soluzione è stata aggiunta agli elementi dell'interfaccia nel costruttore.

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

     [JsonProperty("Details",NullValueHnadling = NullValueHandling.Ignore)]
     public IDetails Details {get; set;}
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.