Przesyłanie interfejsów do deserializacji w JSON.NET

128

Próbuję ustawić czytnik, który będzie pobierał obiekty JSON z różnych stron internetowych (pomyśl o skrobaniu informacji) i tłumaczył je na obiekty C #. Obecnie używam JSON.NET do procesu deserializacji. Problem, z którym się spotykam, polega na tym, że nie wie, jak obsługiwać właściwości na poziomie interfejsu w klasie. Więc coś z natury:

public IThingy Thing

Spowoduje błąd:

Nie można utworzyć instancji typu IThingy. Typ jest interfejsem lub klasą abstrakcyjną i nie można go utworzyć.

Jest stosunkowo ważne, aby był IThingy w przeciwieństwie do Thingy, ponieważ kod, nad którym pracuję, jest uważany za wrażliwy, a testowanie jednostkowe jest bardzo ważne. Mockowanie obiektów dla skryptów testów atomowych nie jest możliwe w przypadku pełnoprawnych obiektów, takich jak Thingy. Muszą być interfejsem.

Od jakiegoś czasu zastanawiam się nad dokumentacją JSON.NET, a pytania, które mogłem znaleźć na tej stronie, pochodzą sprzed ponad roku. Jakaś pomoc?

Ponadto, jeśli ma to znaczenie, moja aplikacja jest napisana w .NET 4.0.

tmesser
źródło

Odpowiedzi:

115

@SamualDavis zapewnił świetne rozwiązanie w powiązanym pytaniu , które podsumuję tutaj.

Jeśli musisz deserializować strumień JSON do konkretnej klasy, która ma właściwości interfejsu, możesz dołączyć konkretne klasy jako parametry do konstruktora dla klasy! Deserializator NewtonSoft jest wystarczająco inteligentny, aby dowiedzieć się, że musi użyć tych konkretnych klas do deserializacji właściwości.

Oto przykład:

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
źródło
15
Jak by to działało z ICollection? ICollection <IGuest> Guests {get; set;}
DrSammyD,
12
Działa z ICollection <ConcreteClass>, więc działa ICollection <Guest>. Podobnie jak FYI, możesz umieścić atrybut [JsonConstructor] w swoim konstruktorze, aby używał go domyślnie, jeśli masz wiele konstruktorów
DrSammyD
6
Utknąłem w tym samym problemie, w moim przypadku mam kilka implementacji interfejsu (w twoim przykładzie jest to ILocation), więc co jeśli istnieją klasy takie jak MyLocation, VIPLocation, OrdinaryLocation. Jak zmapować je do właściwości lokalizacji? Jeśli masz tylko jedną implementację, taką jak MyLocation, jest to łatwe, ale jak to zrobić, jeśli istnieje wiele implementacji ILocation?
ATHER
10
Jeśli masz więcej niż jednego konstruktora, możesz oznaczyć swój specjalny konstruktor [JsonConstructor]atrybutem.
Dr Rob Lang
26
To wcale nie jest w porządku. Celem używania interfejsów jest użycie iniekcji zależności, ale robiąc to z parametrem typu obiektowego wymaganym przez konstruktora, całkowicie zepsujesz punkt posiadania interfejsu jako właściwości.
Jérôme MEVEL
57

(Skopiowano z tego pytania )

W przypadkach, w których nie miałem kontroli nad przychodzącym kodem JSON (i nie mogę zapewnić, że zawiera on właściwość $ type), napisałem niestandardowy konwerter, który pozwala tylko jawnie określić konkretny typ:

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

Po prostu używa domyślnej implementacji serializatora z Json.Net, podczas jawnego określania konkretnego typu.

Omówienie jest dostępne w tym poście na blogu . Kod źródłowy jest poniżej:

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
źródło
11
Bardzo podoba mi się to podejście i zastosowałem je w naszym własnym projekcie. Dodałem nawet ConcreteListTypeConverter<TInterface, TImplementation>do obsługi członków klasy typu IList<TInterface>.
Oliver
3
To świetny fragment kodu. concreteTypeConverterJednak lepiej byłoby mieć rzeczywisty kod w pytaniu.
Chris
2
@Oliver - Czy możesz opublikować swoją ConcreteListTypeConverter<TInterface, TImplementation>implementację?
Michael
2
A jeśli masz dwóch realizatorów ISomething?
bdaniel7
56

Dlaczego warto używać konwertera? W programie dostępna jest natywna funkcjonalnośćNewtonsoft.Json rozwiązania tego problemu:

Ustaw TypeNameHandlingw JsonSerializerSettingstoTypeNameHandling.Auto

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

Spowoduje to umieszczenie każdego typu w jsonie, który nie jest przechowywany jako konkretna instancja typu, ale jako interfejs lub klasa abstrakcyjna.

Upewnij się, że używasz tych samych ustawień do serializacji i deserializacji .

Przetestowałem to i działa jak urok, nawet z listami.

Wyniki wyszukiwania Wynik sieciowy z linkami do witryn

⚠️ OSTRZEŻENIE :

Używaj tego tylko dla json ze znanego i zaufanego źródła. Użytkownik snipsnipsnip poprawnie wspomniał, że jest to rzeczywiście podatność na zarabianie.

Więcej informacji można znaleźć w CA2328 i SCS0028 .


Źródło i alternatywna implementacja ręczna: Blog Code Inside

Mafii
źródło
3
Idealnie, pomogło mi to w szybkim i brudnym głębokim klonie ( stackoverflow.com/questions/78536/deep-cloning-objects )
Compufreak
1
@Shimmy Objects: „Uwzględnij nazwę typu .NET podczas serializacji do struktury obiektu JSON”. Auto: dołącz nazwę typu .NET, jeśli typ serializowanego obiektu nie jest taki sam, jak zadeklarowany typ. Należy zauważyć, że domyślnie nie obejmuje to zserializowanego obiektu głównego. Aby dołączyć nazwę typu obiektu głównego do formatu JSON, należy określić obiekt typu głównego za pomocą SerializeObject (Object, Type, JsonSerializerSettings) lub Serialize (JsonWriter, Object, Type). ”Źródło: newtonsoft.com/json/help/html/…
Mafii
4
Właśnie wypróbowałem to na Deserializacji i to nie działa. Temat tego pytania o przepełnienie stosu brzmi: „Przesyłanie interfejsów do deserializacji w JSON.NET”
Justin Russo
3
@JustinRusso działa tylko wtedy, gdy json został zserializowany z tym samym ustawieniem
Mafii
3
Głosuj za szybkim, jeśli nie brudnym rozwiązaniem. Jeśli tylko serializujesz konfiguracje, to działa. Bije zatrzymanie rozwoju w celu zbudowania konwerterów i na pewno pokonuje dekorowanie każdej wstrzykniętej nieruchomości. serializer.TypeNameHandling = TypeNameHandling.Auto; JsonConvert.DefaultSettings (). TypeNameHandling = TypeNameHandling.Auto;
Sean Anderson,
39

Aby włączyć deserializację wielu implementacji interfejsów, możesz użyć JsonConverter, ale nie za pomocą atrybutu:

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

DTOJsonConverter mapuje każdy interfejs za pomocą konkretnej implementacji:

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 jest wymagany tylko w przypadku deserializatora. Proces serializacji pozostaje niezmieniony. Obiekt Json nie musi osadzać konkretnych nazw typów.

Ten post SO oferuje to samo rozwiązanie o krok dalej z ogólnym JsonConverter.

Eric Boumendil
źródło
Czy wywołanie metody WriteJson do serializer.Serialize nie spowodowałoby przepełnienia stosu, ponieważ wywołanie serialize na wartości serializowanej przez konwerter spowodowałoby ponowne wywołanie metody WriteJson konwertera w sposób rekurencyjny?
Triynko
Nie powinno, jeśli metoda CanConvert () zwraca spójny wynik.
Eric Boumendil
3
Dlaczego porównujesz FullNames, skoro możesz bezpośrednio porównać typy?
Alex Zhukovskiy
Porównywanie typów również jest w porządku.
Eric Boumendil
23

Użyj tej klasy do mapowania typu abstrakcyjnego na typ rzeczywisty:

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

... i podczas deserializacji:

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

        JsonConvert.DeserializeObject(json, type, settings);
Gildor
źródło
1
Bardzo podoba mi się ładna, zwięzła odpowiedź, która rozwiązuje mój problem. Nie ma potrzeby korzystania z autofac ani niczego!
Ben Power,
3
Warto to umieścić w deklaracji klasy konwertera: where TReal : TAbstractaby upewnić się, że może rzutować na typ
Artemious
1
Bardziej kompletny, gdzie może być where TReal : class, TAbstract, new().
Erik Philips,
2
Użyłem tego konwertera również ze struct, uważam, że wystarczy "gdzie TReal: TAbstract". Dziękuje wszystkim.
Gildor,
2
Złoto! Świetna droga.
SwissCoder
12

Nicholas Westby zapewnił świetne rozwiązanie w niesamowitym artykule .

Jeśli chcesz deserializować JSON do jednej z wielu możliwych klas, które implementują taki interfejs:

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

Możesz użyć niestandardowego konwertera JSON:

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

Musisz też ozdobić właściwość „Profession” atrybutem JsonConverter, aby poinformować go o używaniu niestandardowego konwertera:

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

Następnie możesz przesłać swoją klasę za pomocą interfejsu:

Person person = JsonConvert.DeserializeObject<Person>(jsonString);
A. Morel
źródło
8

Możesz spróbować dwóch rzeczy:

Zaimplementuj model try / parse:

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

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

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

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

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

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

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

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

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

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

    return richDude;
  }

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

Lub, jeśli możesz to zrobić w modelu obiektów, zaimplementuj konkretną klasę bazową między IPerson a obiektami-liśćmi i zdeserializuj do niej.

Pierwsza może potencjalnie zawieść w czasie wykonywania, druga wymaga zmian w modelu obiektowym i homogenizuje dane wyjściowe do najniższego wspólnego mianownika.

mcw
źródło
Model try / parse jest niewykonalny ze względu na skalę, z którą muszę pracować. Muszę wziąć pod uwagę zakres setek obiektów podstawowych z jeszcze setkami obiektów pośredniczących / pomocniczych, które reprezentują osadzone obiekty JSON, które często się zdarzają. Zmiana modelu obiektowego nie jest wykluczona, ale czy użycie konkretnej klasy bazowej we właściwościach nie uniemożliwiłoby nam mockowania elementów do testów jednostkowych? A może jakoś się cofam?
tmesser
Nadal możesz zaimplementować makietę z IPerson - zwróć uwagę, że typ właściwości Organisation.Owner to nadal IPerson. Ale w celu deserializacji dowolnego celu musisz zwrócić konkretny typ. Jeśli nie jesteś właścicielem definicji typu i nie możesz zdefiniować minimalnego zestawu właściwości, których będzie wymagał Twój kod, ostatnią deską ratunku jest coś w rodzaju zestawu kluczy / wartości. Używając swojego przykładowego komentarza na Facebooku - czy możesz zamieścić w odpowiedzi, jak wyglądają Twoje (jedno lub kilka) implementacji ILocation? To może pomóc posunąć sprawy naprzód.
mcw
Ponieważ główną nadzieją jest kpina, interfejs ILocation jest w rzeczywistości jedynie fasadą dla konkretnego obiektu Location. Szybki przykład, który właśnie opracowałem, to coś takiego ( pastebin.com/mWQtqGnB ) dla interfejsu i to ( pastebin.com/TdJ6cqWV ) dla konkretnego obiektu.
tmesser
Aby przejść do następnego kroku, oto przykład tego, jak wyglądałby IPage ( pastebin.com/iuGifQXp ) i strona ( pastebin.com/ebqLxzvm ). Problem polega oczywiście na tym, że chociaż deserializacja Page na ogół działałaby dobrze, dławi się, gdy dotrze do właściwości ILocation.
tmesser
Ok, więc myśląc o obiektach, które faktycznie skrobujesz i deserializujesz - czy generalnie jest tak, że dane JSON są zgodne z jedną definicją konkretnej klasy? Czyli (hipotetycznie) nie napotkałbyś „lokalizacji” z dodatkowymi właściwościami, które sprawiłyby, że lokalizacja nie byłaby odpowiednia do użycia jako konkretnego typu dla deserializowanego obiektu? Jeśli tak, przypisanie właściwości ILocation Page za pomocą parametru „LocationConverter” powinno działać. Jeśli nie, a dzieje się tak dlatego, że dane JSON nie zawsze są zgodne ze sztywną lub spójną strukturą (jak ILocation), to (... ciąg dalszy)
mcw
8

Znalazłem to przydatne. Ty też możesz.

Przykładowe użycie

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

Niestandardowy konwerter tworzenia

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

Dokumentacja Json.NET

smiggleworth
źródło
1
Nie jest to wykonalne rozwiązanie. Nie dotyczy list i prowadzi do spryskiwania wszędzie dekoratorów / adnotacji.
Sean Anderson,
5

Dla tych, którzy mogą być ciekawi ConcreteListTypeConverter, do którego odwoływał się Oliver, oto moja próba:

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.
źródło
1
Jestem zdezorientowany z przesłoniętym CanConvert(Type objectType) { return true;}. Wydaje się, że to hack, jak dokładnie to jest pomocne? Może się mylę, ale czy to nie jest jak mówienie mniejszemu niedoświadczonemu wojownikowi, że wygrają walkę bez względu na przeciwnika?
Chef_Code
4

Bez względu na to, co jest warte, w większości musiałem sobie z tym poradzić. Każdy obiekt ma metodę Deserialize (string jsonStream) . Kilka jego fragmentów:

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

W tym przypadku new Thingy (string) jest konstruktorem, który wywoła metodę Deserialize (string jsonStream) odpowiedniego typu konkretnego. Ten schemat będzie kontynuowany w dół i w dół, aż dojdziesz do punktów bazowych, które może obsłużyć json.NET.

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

Itd. itp. Ta konfiguracja pozwoliła mi podać konfiguracje json.NET, które może obsłużyć bez konieczności refaktoryzacji dużej części samej biblioteki lub używania nieporęcznych modeli try / parse, które ugrzęzłyby w całej naszej bibliotece ze względu na liczbę zaangażowanych obiektów. Oznacza to również, że mogę skutecznie obsłużyć wszelkie zmiany json na określonym obiekcie i nie muszę się martwić o wszystko, czego dotyka obiekt. W żadnym wypadku nie jest to idealne rozwiązanie, ale działa całkiem dobrze na podstawie naszych testów jednostkowych i integracyjnych.

tmesser
źródło
4

Załóżmy, że ustawienie autofac wygląda następująco:

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

Następnie załóżmy, że twoja klasa wygląda tak:

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

Dlatego użycie resolwera w deserializacji może wyglądać następująco:

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

Więcej szczegółów można znaleźć pod adresem http://www.newtonsoft.com/json/help/html/DeserializeWithDependencyInjection.htm

O mój Boże
źródło
Zagłosuję za najlepszym rozwiązaniem. DI był tak szeroko używany w dzisiejszych czasach przez twórców stron internetowych C #, a to dobrze pasuje jako scentralizowane miejsce do obsługi konwersji typów przez program rozpoznawania nazw.
appletwo
3

Żaden obiekt nigdy nie będzie IThingy, ponieważ wszystkie interfejsy są z definicji abstrakcyjne.

Przedmiotem masz że po raz pierwszy w odcinkach było jakiejś betonowej typu realizacji abstrakcyjny interfejs. Musisz mieć tę samą konkretną klasę ożywiającą zserializowane dane.

Powstały obiekt będzie wtedy pewnego rodzaju, który implementuje abstrakcyjny interfejs, którego szukasz.

Z dokumentacji wynika, że ​​możesz użyć

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

podczas deserializacji, aby poinformować JSON.NET o konkretnym typie.

Sean Kinsey
źródło
To jest właśnie post sprzed ponad roku, o którym mówiłem. Jedyna ważna sugestia (pisanie niestandardowych konwerterów) nie jest strasznie wykonalna w skali, którą jestem zmuszony rozważyć. JSON.NET bardzo się zmienił w międzyczasie. Doskonale rozumiem różnicę między klasą a interfejsem, ale C # obsługuje również niejawne konwersje z interfejsu do obiektu, który implementuje interfejs w odniesieniu do pisania. Zasadniczo pytam, czy istnieje sposób, aby powiedzieć JSON.NET, który obiekt zaimplementuje ten interfejs.
tmesser
To wszystko było w odpowiedzi, na którą ci wskazałem. Upewnij się, że istnieje _typewłaściwość sygnalizująca konkretny typ do użycia.
Sean Kinsey,
I mocno wątpię, czy C # obsługuje jakiekolwiek „niejawne” rzutowanie typów ze zmiennej zadeklarowanej jako interfejs do konkretnego typu bez żadnych wskazówek.
Sean Kinsey,
O ile nie przeczytałem tego źle, właściwość _type miała znajdować się w formacie JSON, aby zostać zserializowanym. Działa to dobrze, jeśli deserializujesz tylko to, co już zserializowałeś, ale to nie jest to, co się tutaj dzieje. Pobieram JSON z wielu witryn, które nie będą zgodne z tym standardem.
tmesser
@YYY - czy kontrolujesz zarówno serializację do, jak i deserializację ze źródłowego JSON? Ponieważ ostatecznie będziesz musiał osadzić konkretny typ w zserializowanym formacie JSON jako wskazówkę do użycia podczas deserializacji lub będziesz musiał użyć pewnego rodzaju modelu try / parse, który wykrywa / próbuje wykryć konkretny typ w czasie wykonywania i wywołaj odpowiedni deserializator.
mcw
3

Moje rozwiązanie tego, które mi się podoba, bo jest ładnie ogólne, jest następujące:

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

}

Można oczywiście i trywialnie przekonwertować go na jeszcze bardziej ogólny konwerter, dodając konstruktor, który pobierał argument typu Dictionary <Type, Type>, za pomocą którego można utworzyć wystąpienie zmiennej wystąpienia konwersji.

Simon Brooke
źródło
3

Kilka lat później miałem podobny problem. W moim przypadku były mocno zagnieżdżone interfejsy i preferencja generowania konkretnych klas w czasie wykonywania, tak aby działała z klasą generyczną.

Zdecydowałem się utworzyć klasę proxy w czasie wykonywania, która otacza obiekt zwracany przez Newtonsoft.

Zaletą tego podejścia jest to, że nie wymaga ono konkretnej implementacji klasy i może automatycznie obsługiwać dowolną głębokość zagnieżdżonych interfejsów. Więcej na ten temat można przeczytać na moim blogu .

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

        }
    }



}

Stosowanie:

var jObj = JObject.Parse(input);
InterfaceType proxyObject = jObj.toProxy<InterfaceType>();
Sudsy
źródło
Dzięki! Jest to jedyna odpowiedź, która poprawnie obsługuje dynamiczne typowanie (typowanie typu kaczego) bez narzucania ograniczeń na przychodzące pliki json.
Philip Pittle
Nie ma problemu. Byłem trochę zaskoczony, widząc, że nic tam nie ma. Od czasu tego oryginalnego przykładu trochę się zmieniło, więc zdecydowałem się udostępnić kod. github.com/sudsy/JsonDuckTyper . Opublikowałem go również na nuget jako JsonDuckTyper. Jeśli uznasz, że chcesz to ulepszyć, po prostu wyślij mi PR, a ja z przyjemnością Ci pomogę.
Sudsy
Szukając rozwiązania w tym zakresie trafiłem również na github.com/ekonbenefits/impromptu-interface . W moim przypadku nie działa, ponieważ nie obsługuje dotnet core 1.0, ale może działać dla Ciebie.
Sudsy
Próbowałem z Impromptu Interface, ale Json.Net nie był zadowolony z robienia PopulateObjectna proxy generowanym przez Impromptu Interface. Niestety zrezygnowałem z Duck Typing - po prostu łatwiej było stworzyć niestandardowy Serializer kontraktu Json, który wykorzystywał odbicie, aby znaleźć istniejącą implementację żądanego interfejsu i użyć tego.
Philip Pittle,
1

Użyj tego JsonKnownTypes , jest to bardzo podobny sposób użycia, po prostu dodaj dyskryminator do json:

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

Teraz podczas serializacji obiekt w json zostanie dodany "$type"z "myClass"wartością i będzie używany do deserializacji

Json:

{"Something":"something", "$type":"derived"}
Dmitry
źródło
0

W moim rozwiązaniu dodano elementy interfejsu w konstruktorze.

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

     [JsonProperty("Details",NullValueHnadling = NullValueHandling.Ignore)]
     public IDetails Details {get; set;}
}
Jorge Santos Neill
źródło