Używanie konwerterów Json.NET do deserializacji właściwości

88

Mam definicję klasy, która zawiera właściwość, która zwraca interfejs.

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

    public ISomething Thing { get; set; }
}

Próba serializacji klasy Foo przy użyciu Json.NET powoduje wyświetlenie komunikatu o błędzie, takiego jak „Nie można utworzyć instancji typu 'ISomething'. ISomething może być interfejsem lub klasą abstrakcyjną”.

Czy istnieje atrybut lub konwerter Json.NET, który pozwoliłby mi określić konkretną Somethingklasę do użycia podczas deserializacji?

dthrasher
źródło
Uważam, że musisz określić nazwę właściwości, która pobiera / ustawia ISocoś
ram
Mam. Używam skrótu dla właściwości zaimplementowanych automatycznie wprowadzonych w C # 3.5. msdn.microsoft.com/en-us/library/bb384054.aspx
dthrasher
4
Czy to nie jest coś w rodzaju. Myślę, że baran ma rację, nadal potrzebujesz nazwy nieruchomości. Wiem, że to nie jest związane z twoim problemem, ale twój komentarz powyżej sprawił, że pomyślałem, że brakuje mi jakiejś nowej funkcji w .NET, która pozwoliłaby ci określić właściwość bez nazwy.
Pan Łoś

Odpowiedzi:

92

Jedną z rzeczy, które możesz zrobić z Json.NET jest:

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

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

TypeNameHandlingFlag doda $typeobiekt do JSON, który pozwala Json.NET wiedzieć, który konkretny typ musi deserializacji obiektu do. Pozwala to na deserializację obiektu, jednocześnie spełniając interfejs lub abstrakcyjną klasę bazową.

Wadą jest jednak to, że jest to bardzo specyficzne dla Json.NET. $typeBędzie w pełni kwalifikowaną typ, więc jeśli szeregowania go z informacją typu ,, potrzeby Deserializator aby móc je zrozumieć, jak również.

Dokumentacja: ustawienia serializacji w Json.NET

Daniel T.
źródło
Ciekawy. Będę musiał się tym bawić. Niezła wskazówka!
dthrasher
2
W przypadku Newtonsoft.Json działa podobnie, ale właściwość to „$ type”
Jaap,
To było zbyt łatwe!
Shimmy Weitzhandler
1
Uważaj na możliwe problemy z bezpieczeństwem podczas korzystania z TypeNameHandling. Aby uzyskać szczegółowe informacje, zobacz przestroga TypeNameHandling w Newtonsoft Json .
dbc
Wczoraj walczyłem jak szalony z konwerterami, a to było o wiele lepsze i lepiej zrozumiałe, dzięki !!!
Horothenic
52

Możesz to osiągnąć za pomocą klasy JsonConverter. Załóżmy, że masz klasę z właściwością interfejsu;

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

Twój JsonConverter jest odpowiedzialny za serializację i deserializację podstawowej właściwości;

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

Podczas pracy z organizacją zdeserializowaną za pośrednictwem Json.Net bazowy IPerson dla właściwości Owner będzie typu Tycoon.

MrMDavidson
źródło
Bardzo dobrze. Muszę spróbować konwertera.
dthrasher
4
Czy tag „[JsonConverter (typeof (TycoonConverter))]” nadal działałby, gdyby znajdował się na liście interfejsu?
Zwik
40

Zamiast przekazywać dostosowany obiekt JsonSerializerSettings do JsonConvert.SerializeObject () z opcją TypeNameHandling.Objects, jak wspomniano wcześniej, można po prostu oznaczyć tę właściwość interfejsu atrybutem, aby wygenerowany kod JSON nie był rozdęty właściwościami „$ type” na KAŻDYM obiekcie:

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
źródło
Znakomity. Dzięki :)
Darren Young
5
W przypadku kolekcji interfejsów lub klas abstrakcyjnych właściwość to „ItemTypeNameHandling”. np .: [JsonProperty (ItemTypeNameHandling = TypeNameHandling.Auto)]
Anthony F
Dziękuję Ci za to!
brudert
23

W najnowszej wersji konwertera Newtonsoft Json innej firmy można ustawić konstruktor z konkretnym typem odnoszącym się do właściwości interfejsu.

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

Dopóki Coś implementuje I coś to powinno działać. Nie umieszczaj również domyślnego pustego konstruktora na wypadek, gdyby konwerter JSon próbował go użyć, musisz wymusić na nim użycie konstruktora zawierającego konkretny typ.

PS. pozwala to również ustawić setery jako prywatne.

SamuelDavis
źródło
6
Powinno to krzyczeć z dachów! To prawda, że ​​nakłada ograniczenia na konkretną implementację, ale jest o wiele prostsza niż inne podejścia w sytuacjach, w których można ją zastosować.
Mark Meuer
3
A jeśli mamy więcej niż jednego konstruktora z wieloma typami betonu, czy nadal będzie wiedział?
Teoman shipahi
1
Ta odpowiedź jest tak elegancka w porównaniu do wszystkich zawiłych bzdur, które musiałbyś zrobić inaczej. To powinna być akceptowana odpowiedź. Jedynym zastrzeżeniem w moim przypadku było to, że musiałem dodać [JsonConstructor] przed konstruktorem, aby działał .... Podejrzewam, że użycie tego tylko na JEDNYM z twoich konstruktorów betonowych rozwiązałoby twój (4-letni) problem @Teomanshipahi
nacitar sevaht
@nacitarsevaht Mogę wrócić i teraz naprawić mój problem :) w każdym razie nawet nie pamiętam, co to było, ale kiedy ponownie przeglądam, jest to dobre rozwiązanie w niektórych przypadkach.
Teoman shipahi
również tego używamy, ale w większości przypadków wolę konwertować, ponieważ połączenie konkretnego typu z konstruktorem niweczy cel użycia interfejsu dla właściwości w pierwszej kolejności!
gabe
19

Miałem ten sam problem, więc wymyśliłem własny konwerter, który używa argumentu znanych typów.

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

Zdefiniowałem dwie metody rozszerzające do deserializacji i serializacji:

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

Możesz zdefiniować własny sposób porównywania i identyfikowania typów w konwerterach, używam tylko nazwy klasy.

Bruno Altinet
źródło
1
Ten JsonConverter jest świetny, użyłem go, ale napotkałem kilka problemów, które rozwiązałem w ten sposób: - Używając JsonSerializer.CreateDefault () zamiast wypełnić, ponieważ mój obiekt miał głębszą hierarchię. - Wykorzystanie refleksji do odzyskania konstruktora i Instanciate go w metodzie Create ()
Aurel
3

Zwykle zawsze korzystałem z rozwiązania TypeNameHandlingzgodnie z sugestią DanielT, ale w przypadkach tutaj nie miałem kontroli nad przychodzącym kodem JSON (i dlatego nie mogę zapewnić, że zawiera on $typewłaściwość) Napisałem niestandardowy konwerter, który pozwala tylko wyraźnie określić rodzaj betonu:

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

To po prostu używa domyślnej implementacji serializatora z Json.Net, jednocześnie jawnie określając konkretny typ.

Kod źródłowy i przegląd są dostępne w tym poście na blogu .

Steve Greatrex
źródło
1
To świetne rozwiązanie. Twoje zdrowie.
JohnMetta
2

Chciałem tylko zakończyć przykład, który @Daniel T. pokazał nam powyżej:

Jeśli używasz tego kodu do serializacji obiektu:

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

Kod do deserializacji json powinien wyglądać następująco:

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

Oto jak json jest dostosowywany przy użyciu TypeNameHandlingflagi:wprowadź opis obrazu tutaj

Luis Armando
źródło
-5

Zastanawiałem się nad tym samym, ale obawiam się, że nie da się tego zrobić.

Spójrzmy na to w ten sposób. Przekazujesz JSon.net ciąg danych i typ do deserializacji. Co ma zrobić JSON.net, gdy trafi w coś takiego? Nie może stworzyć nowego typu ISomething, ponieważ ISomething nie jest obiektem. Nie może również stworzyć obiektu, który implementuje ISomething, ponieważ nie ma pojęcia, który z wielu obiektów może dziedziczyć ICoś, czego powinien użyć. Interfejsy to coś, co można automatycznie serializować, ale nie można automatycznie deserializować.

Chciałbym przyjrzeć się zamianie ISomething na klasę bazową. Używając tego, możesz uzyskać efekt, którego szukasz.

Timothy Baldridge
źródło
1
Zdaję sobie sprawę, że to nie zadziała „po wyjęciu z pudełka”. Ale zastanawiałem się, czy istnieje jakiś atrybut, taki jak „[JsonProperty (typeof (SomethingBase))]”, którego mógłbym użyć w celu zapewnienia konkretnej klasy.
dthrasher
Dlaczego więc nie użyć SomethingBase zamiast ISomething w powyższym kodzie? Można by argumentować, że my również patrzymy na to w niewłaściwy sposób, ponieważ interfejsy nie powinny być używane w serializacji, ponieważ definiują one po prostu "interfejs" komunikacyjny z daną klasą. Serializacja interfejsu jest technicznie bezsensowna, podobnie jak serializacja klasy abstrakcyjnej. Więc chociaż „można to zrobić”, twierdzę, że „nie powinno się tego robić”.
Timothy Baldridge
Czy przyjrzałeś się którejś z klas w przestrzeni nazw Newtonsoft.Json.Serialization? szczególnie klasa JsonObjectContract?
johnny
-9

Oto odniesienie do artykułu napisanego przez ScottGu

Na tej podstawie napisałem kod, który moim zdaniem może być pomocny

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

I tak można to nazwać

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}

Jeśli dobrze to rozumiem, nie sądzę, aby trzeba było określać konkretną klasę, która implementuje interfejs do serializacji JSON.

Baran
źródło
1
Twój przykład używa JavaScriptSerializer, klasy w .NET Framework. Używam Json.NET jako mojego serializatora. codeplex.com/Json
dthrasher
3
Nie odnosi się do pierwotnego pytania, wyraźnie wspomniano tam o Json.NET.
Oliver,