Vraag JSON-serialisatie van array met polymorfe objecten


Is het mogelijk met .NET-standaard JavascriptSerializer / JsonDataContractSerializer of externe parsers, om array van objecten te serialiseren met behulp van een wrapper-aanpak inclusief het objecttype?

Bijvoorbeeld om deze JSON vanuit een lijst te genereren:

[{ 'dog': { ...dog properties... } },
 { 'cat': { ...cat properties... } }]

in plaats van typisch:

[{ ...dog properties... },
 { ...cat properties... }]

Dit kan in Java worden gedaan met Jackson met het kenmerk JsonTypeInfo.As.WRAPPER_OBJECT.


15
2018-03-03 21:45


oorsprong


antwoorden:


Json.NET heeft hiervoor een nette oplossing. Er is een instelling die op intelligente wijze typegegevens toevoegt - verklaar het als volgt:

new JsonSerializer { TypeNameHandling = TypeNameHandling.Auto };

Dit zal bepalen of type embedding vereist is en voeg deze waar nodig toe. Laten we zeggen dat ik de volgende klassen had:

public class Message
{
    public object Body { get; set; }
}

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

public class Manager : Person
{

}

public class Department
{
    private List<Person> _employees = new List<Person>();
    public List<Person> Employees { get { return _employees; } }
}

Merk op dat de Message Body van het type object is, en dat Manager subpersoon Persoon is. Als ik een bericht serialiseer met een afdelingshoofd met een enkele manager, krijg ik dit:

{
    "Body":
    {
        "$type":"Department, MyAssembly",
        "Employees":[
            {
                "$type":"Manager, MyAssembly",
                "Name":"Tim"
            }]
    }
}

Merk op hoe het de $ -type-eigenschap heeft toegevoegd om de afdelings- en beheertypen te beschrijven. Als ik nu een Persoon aan de lijst Werknemers toevoeg en het berichtgedeelte van het type Afdeling als volgt verander:

public class Message
{
    public Department Body { get; set; }
}

dan is de annotatie van het Body Type niet langer nodig en wordt de nieuwe Persoon niet geannoteerd - afwezigheid van annotatie gaat ervan uit dat het element instantie van het aangegeven array type is. Het serialized formaat wordt:

{
    "Body":
    {
        "Employees":[
            {
                "$type":"Manager, MyAssembly",
                "Name":"Tim"
            },
            {
                "Name":"James"
            }]
    }
}

Dit is een efficiënte aanpak - typeannotatie wordt alleen toegevoegd waar dat nodig is. Hoewel dit .NET-specifiek is, is de aanpak eenvoudig genoeg om dat deserializers / berichttypen op andere platforms vrij eenvoudig uit te breiden om hiermee om te gaan.

Ik zou echter terughoudend zijn om dit in een openbare API te gebruiken, omdat het niet-standaard is. In dat geval zou u polymorfisme willen vermijden, en versiebeheer en type-informatie zeer expliciete eigenschappen in het bericht willen maken.


21
2017-07-17 08:49



Waarschijnlijk het dichtst dat ik heb gezien is het gebruik van de JavaScriptSerializer en geef over in a JavaScriptTypeResolver aan de constructeur. Het produceert niet JSON precies zoals je het hebt in je vraag, maar het heeft wel een _type veld dat het type object beschrijft dat geserialiseerd wordt. Het kan een beetje lelijk worden, maar misschien gaat het je wel helpen.

Hier is mijn voorbeeldcode:

public abstract class ProductBase
{
    public String Name { get; set; }
    public String Color { get; set; }
}

public class Drink : ProductBase
{
}

public class Product : ProductBase
{
}

class Program
{
    static void Main(string[] args)
    {
        List<ProductBase> products = new List<ProductBase>()
        {
            new Product() { Name="blah", Color="Red"},
            new Product(){ Name="hoo", Color="Blue"},
            new Product(){Name="rah", Color="Green"},
            new Drink() {Name="Pepsi", Color="Brown"}
        };

        JavaScriptSerializer ser = new JavaScriptSerializer(new SimpleTypeResolver());

        Console.WriteLine(ser.Serialize(products));    
    }
}

En het resultaat ziet er als volgt uit:

[
  {"__type":"TestJSON1.Product, TestJSON1, Version=1.0.0.0, Culture=neutral, Publ
icKeyToken=null","Name":"blah","Color":"Red"},
  {"__type":"TestJSON1.Product, Test
JSON1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null","Name":"hoo","Colo
r":"Blue"},
  {"__type":"TestJSON1.Product, TestJSON1, Version=1.0.0.0, Culture=neu
tral, PublicKeyToken=null","Name":"rah","Color":"Green"},
  {"__type":"TestJSON1.Dr
ink, TestJSON1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null","Name":"P
epsi","Color":"Brown"}
]

Ik gebruik de SimpleTypeConverter, die standaard deel uitmaakt van het framework. U kunt uw eigen maken om in te korten wat wordt geretourneerd __type.

BEWERK: Als ik mijn eigen maak JavaScriptTypeResolver om de geretourneerde type naam in te korten, kan ik zoiets als dit produceren:

[
  {"__type":"TestJSON1.Product","Name":"blah","Color":"Red"},
  {"__type":"TestJSON1.Product","Name":"hoo","Color":"Blue"},
  {"__type":"TestJSON1.Product","Name":"rah","Color":"Green"},
  {"__type":"TestJSON1.Drink","Name":"Pepsi","Color":"Brown"}
]

Met behulp van deze conversieklasse:

public class MyTypeResolver : JavaScriptTypeResolver
{
    public override Type ResolveType(string id)
    {
        return Type.GetType(id);
    }

    public override string ResolveTypeId(Type type)
    {
        if (type == null)
        {
            throw new ArgumentNullException("type");
        }

        return type.FullName;
    }
}

En gewoon doorgeven aan mijn JavaScriptSerializer constructor (in plaats van de SimpleTypeConverter).

Ik hoop dat dit helpt!


11
2018-03-03 22:18



1) U kunt een Dictionary <string, object> gebruiken om de klus te klaren, ...

[{ "Cat": { "Naam": "Pinky"}}, { "Cat": { "Naam": "Winky"}}, { "Dog": { "Naam": "Max"}}]

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

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


    internal static void Main()
    {
        List<object> animals = new List<object>();
        animals.Add(new Cat() { Name = "Pinky" });
        animals.Add(new Cat() { Name = "Winky" });
        animals.Add(new Dog() { Name = "Max" });
        // Convert every item in the list into a dictionary
        for (int i = 0; i < animals.Count; i++)
        {
            var animal = new Dictionary<string, object>();
            animal.Add(animals[i].GetType().Name, animals[i]);
            animals[i] = animal;
        }
        var serializer = new JavaScriptSerializer();
        var json = serializer.Serialize(animals.ToArray());


        animals = (List<object>)serializer.Deserialize(json, animals.GetType());
        // convert every item in the dictionary back into a list<object> item
        for (int i = 0; i < animals.Count; i++)
        {
            var animal = (Dictionary<string, object>)animals[i];
            animal = (Dictionary<string, object>)animal.Values.First();
            animals[i] = animal.Values.First();
        }
    }

2) Of met behulp van de JavaScriptConverter is het mogelijk om de serialisatie voor een type aan te pakken.

[{ "Kat": { "Omnivoor": true}}, { "aardvarken": { "Insectivore": true}}, { "aardvarken": { "Insectivore": true}}]

abstract class AnimalBase { }

class Aardvark : AnimalBase
{
    public bool Insectivore { get; set; }
}

class Dog : AnimalBase
{
    public bool Omnivore { get; set; }
}

class AnimalsConverter : JavaScriptConverter
{
    private IDictionary<string, Type> map;

    public AnimalsConverter(IDictionary<string, Type> map) { this.map = map; }

    public override IEnumerable<Type> SupportedTypes
    {
        get { return new Type[]{typeof(AnimalBase)}; }
    }

    public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
    {
        var result = new Dictionary<string, object>();
        var type = obj.GetType();
        var name = from x in this.map where x.Value == type select x.Key;
        if (name.Count<string>() == 0)
            return null;
        var value = new Dictionary<string, object>();
        foreach (var prop in type.GetProperties())
        {
            if(!prop.CanRead) continue;
            value.Add(prop.Name, prop.GetValue(obj, null));
        }
        result.Add(name.First<string>(), value);
        return result;
    }

    public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)
    {
        var keys = from x in this.map.Keys where dictionary.ContainsKey(x) select x;
        if (keys.Count<string>() <= 0) return null;
        var key = keys.First<string>();
        var poly = this.map[key];
        var animal = (AnimalBase)Activator.CreateInstance(poly);
        var values = (Dictionary<string, object>)dictionary[key];
        foreach (var prop in poly.GetProperties())
        {
            if(!prop.CanWrite) continue;
            var value = serializer.ConvertToType(values[prop.Name], prop.PropertyType);
            prop.SetValue(animal, value, null);
        }
        return animal;
    }
}

class Program
{
    static void Main(string[] args)
    {
        var animals = new List<AnimalBase>();
        animals.Add(new Dog() { Omnivore = true });
        animals.Add(new Aardvark() { Insectivore = false });
        animals.Add(new Aardvark() { Insectivore = true });
        var convertMap = new Dictionary<string, Type>();
        convertMap.Add("cat", typeof(Dog));
        convertMap.Add("aardvark", typeof(Aardvark));
        var converter = new AnimalsConverter(convertMap);
        var serializer = new JavaScriptSerializer();
        serializer.RegisterConverters(new JavaScriptConverter[] {converter});
        var json = serializer.Serialize(animals.ToArray());
        animals.Clear();
        animals.AddRange((AnimalBase[])serializer.Deserialize(json, typeof(AnimalBase[])));
    }
}

0
2017-11-18 17:24



Ik heb dit gedaan volgens de vraag. Was niet helemaal duidelijk, maar hier gaat het. Er is geen gemakkelijke manier om dit in Json.NET te doen. Zou geweldig zijn als het een pre-serialisatie callback zou ondersteunen waar je je eigen type informatie zou kunnen invoegen, maar dat is een ander verhaal.

Ik heb een interface (IShape) die de polymorfe klassen implementeren. Een van de klassen is een container (samengesteld patroon) en bevat een lijst met ingesloten objecten. Ik deed dit met interfaces, maar hetzelfde concept is van toepassing op basisklassen.

public class Container : IShape
{
    public virtual List<IShape> contents {get;set;}
    // implement interface methods

Zoals aangegeven in de vraag, wil ik dit als volgt serialiseren:

  "container": {
    "contents": [
      {"box": { "TopLeft": {"X": 0.0,"Y": 0.0},"BottomRight": {"X": 1.0, "Y": 1.0} } },
      {"line": {"Start": { "X": 0.0,"Y": 0.0},"End": {"X": 1.0,"Y": 1.0 }} },

enz.

Om dit te doen schreef ik een wrapper-les. Elk van de objecten die de interface implementeren heeft een eigenschap in de verpakking. Hiermee stelt u de naam van de eigenschap in de serializer in. Voorwaardelijke serialisatie zorgt ervoor dat de juiste eigenschap wordt gebruikt. Alle interfacemethoden worden aan de ingepakte klasse gedelegeerd en aanroep Accepteren () wordt doorgestuurd naar de ingepakte klasse. Dit betekent dat in contexten die de interface gebruiken, de klassen Wrapped of unwrapped zich hetzelfde zullen gedragen.

    public class SerializationWrapper : IShape
    {
        [JsonIgnore]
        public IShape Wrapped { get; set; }
        // Accept method for the visitor - redirect visitor to the wrapped class
        // so visitors will behave the same with wrapped or unwrapped.
        public void Accept(IVisitor visitor) => Wrapped.Accept(visitor);

        public bool ShouldSerializeline() => line != null;
        // will serialize as line : { ...
        public Line line { get =>Wrapped as Line;}

        public bool ShouldSerializebox() => box != null;
        public Box box { get => Wrapped as Box; }

        public bool ShouldSerializecontainer() => container != null;
        public Container container { get => Wrapped as Container; }

        // IShape methods delegated to Wrapped
        [JsonIgnore]
        public Guid Id { get => Wrapped.Id; set => Wrapped.Id = value; }

Ik heb ook een bezoekerspatroon geïmplementeerd om de objectgrafiek te doorlopen. Ik had dit al te danken aan de rest van het softwareontwerp, maar als je gewoon een eenvoudige verzameling hebt, kun je gewoon de collectie herhalen en de wrapper toevoegen.

    public class SerializationVisitor : IVisitor
    {
        public void Visit(IContainer shape)
        {
            // replace list items with wrapped list items
            var wrappedContents = new List<IShape>();
            shape.Contents.ForEach(s => { wrappedContents.Add(new SerializationWrapper(){ Wrapped = s}); s.Accept(this); });
            shape.Contents = wrappedContents;
        }

        public void Visit(ILine shape){}
        public void Visit(IBox shape){}
    }

De bezoeker vervangt de inhoud van de klasse Container door ingepakte versies van de klassen.

Serialiseren en produceert de vereiste uitvoer.

        SerializationVisitor s = new SerializationVisitor();
        s.Visit(label);

Omdat ik de bezoeker al heb en alles via interfaces doe, is het waarschijnlijk net zo gemakkelijk om mijn eigen serializer te doen, hoe dan ook .......


0
2017-11-13 18:43