Polimorfizm z gsonem


103

Mam problem z deserializacją łańcucha JSON za pomocą Gson. Otrzymuję szereg poleceń. Poleceniem może być start, stop lub inny typ polecenia. Oczywiście mam polimorfizm, a polecenie start / stop dziedziczy po poleceniu.

Jak mogę serializować go z powrotem do właściwego obiektu polecenia za pomocą gson?

Wydaje się, że otrzymuję tylko typ podstawowy, czyli zadeklarowany typ, a nigdy typ środowiska uruchomieniowego.


Odpowiedzi:


120

Trochę za późno, ale dzisiaj musiałem zrobić dokładnie to samo. Tak więc, opierając się na moich badaniach i używając gson-2.0, naprawdę nie chcesz używać metody registerTypeHierarchyAdapter , ale raczej bardziej przyziemnego registerTypeAdapter . I na pewno nie musisz robić instanceofs ani pisać adapterów dla klas pochodnych: wystarczy jeden adapter dla klasy bazowej lub interfejsu, oczywiście pod warunkiem, że jesteś zadowolony z domyślnej serializacji klas pochodnych. W każdym razie, oto kod (usunięto pakiet i import) (dostępny również na github ):

Klasa bazowa (interfejs w moim przypadku):

public interface IAnimal { public String sound(); }

Dwie klasy pochodne, Cat:

public class Cat implements IAnimal {

    public String name;

    public Cat(String name) {
        super();
        this.name = name;
    }

    @Override
    public String sound() {
        return name + " : \"meaow\"";
    };
}

I pies:

public class Dog implements IAnimal {

    public String name;
    public int ferocity;

    public Dog(String name, int ferocity) {
        super();
        this.name = name;
        this.ferocity = ferocity;
    }

    @Override
    public String sound() {
        return name + " : \"bark\" (ferocity level:" + ferocity + ")";
    }
}

IAnimalAdapter:

public class IAnimalAdapter implements JsonSerializer<IAnimal>, JsonDeserializer<IAnimal>{

    private static final String CLASSNAME = "CLASSNAME";
    private static final String INSTANCE  = "INSTANCE";

    @Override
    public JsonElement serialize(IAnimal src, Type typeOfSrc,
            JsonSerializationContext context) {

        JsonObject retValue = new JsonObject();
        String className = src.getClass().getName();
        retValue.addProperty(CLASSNAME, className);
        JsonElement elem = context.serialize(src); 
        retValue.add(INSTANCE, elem);
        return retValue;
    }

    @Override
    public IAnimal deserialize(JsonElement json, Type typeOfT,
            JsonDeserializationContext context) throws JsonParseException  {
        JsonObject jsonObject = json.getAsJsonObject();
        JsonPrimitive prim = (JsonPrimitive) jsonObject.get(CLASSNAME);
        String className = prim.getAsString();

        Class<?> klass = null;
        try {
            klass = Class.forName(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new JsonParseException(e.getMessage());
        }
        return context.deserialize(jsonObject.get(INSTANCE), klass);
    }
}

I klasa Test:

public class Test {

    public static void main(String[] args) {
        IAnimal animals[] = new IAnimal[]{new Cat("Kitty"), new Dog("Brutus", 5)};
        Gson gsonExt = null;
        {
            GsonBuilder builder = new GsonBuilder();
            builder.registerTypeAdapter(IAnimal.class, new IAnimalAdapter());
            gsonExt = builder.create();
        }
        for (IAnimal animal : animals) {
            String animalJson = gsonExt.toJson(animal, IAnimal.class);
            System.out.println("serialized with the custom serializer:" + animalJson);
            IAnimal animal2 = gsonExt.fromJson(animalJson, IAnimal.class);
            System.out.println(animal2.sound());
        }
    }
}

Po uruchomieniu Test :: main otrzymasz następujące dane wyjściowe:

serialized with the custom serializer:
{"CLASSNAME":"com.synelixis.caches.viz.json.playground.plainAdapter.Cat","INSTANCE":{"name":"Kitty"}}
Kitty : "meaow"
serialized with the custom serializer:
{"CLASSNAME":"com.synelixis.caches.viz.json.playground.plainAdapter.Dog","INSTANCE":{"name":"Brutus","ferocity":5}}
Brutus : "bark" (ferocity level:5)

W rzeczywistości zrobiłem to powyżej przy użyciu metody registerTypeHierarchyAdapter , ale wydawało się, że wymagało to zaimplementowania niestandardowych klas serializatora / deserializatora DogAdapter i CatAdapter, które są trudne do utrzymania za każdym razem, gdy chcesz dodać kolejne pole do Dog lub Cat.


5
Należy zauważyć, że serializacja nazw klas i deserializacja (na podstawie danych wejściowych użytkownika) przy użyciu Class.forName może mieć wpływ na bezpieczeństwo w niektórych sytuacjach, dlatego zespół deweloperów Gson odradza to. code.google.com/p/google-gson/issues/detail?id=340#c2
Programmer Bruce

4
Jak udało ci się nie uzyskać nieskończonej pętli podczas serializacji, wywołujesz context.serialize (src); który będzie ponownie wywoływał twój adapter. To właśnie stało się w moim podobnym kodzie.
che javara

6
Źle. To rozwiązanie nie działa. Jeśli w jakikolwiek sposób wywołasz context.serialize, otrzymasz nieskończoną rekursję. Zastanawiam się, dlaczego ludzie publikują bez testowania kodu. Próbowałem z 2.2.1. Zobacz błąd opisany na stackoverflow.com/questions/13244769/ ...
che javara

4
@MarcusJuniusBrutus Uruchomiłem Twój kod i wygląda na to, że działa tylko w tym szczególnym przypadku - ponieważ zdefiniowałeś superinterfejs IAnimal, a IAnimalAdapter go używa. Jeśli zamiast tego miałbyś tylko 'Cat', otrzymasz problem nieskończonej rekurencji. Więc to rozwiązanie nadal nie działa w ogólnym przypadku - tylko wtedy, gdy jesteś w stanie zdefiniować wspólny interfejs. W moim przypadku nie było interfejsu, więc musiałem użyć innego podejścia z TypeAdapterFactory.
che javara

2
Użytkownik src.getClass (). GetName () zamiast src.getClass (). GetCanonicalName (). Dzięki temu kod będzie działał również dla klas wewnętrznych / zagnieżdżonych.
mR_fr0g

13

Gson ma obecnie mechanizm do rejestrowania adaptera hierarchii typów, który podobno można skonfigurować do prostej deserializacji polimorficznej, ale nie widzę, jak to się dzieje, ponieważ adapter hierarchii typów wydaje się być po prostu połączonym serializatorem / deserializatorem / twórcą instancji, pozostawiając szczegóły tworzenia instancji koderowi, bez podawania rzeczywistej rejestracji typu polimorficznego.

Wygląda na to, że Gson wkrótce będzie miał RuntimeTypeAdapterprostszą deserializację polimorficzną. Więcej informacji można znaleźć pod adresem http://code.google.com/p/google-gson/issues/detail?id=231 .

Jeśli użycie nowego RuntimeTypeAdapternie jest możliwe i musisz użyć Gson, myślę, że będziesz musiał stworzyć własne rozwiązanie, rejestrując niestandardowy deserializator jako adapter hierarchii typów lub jako adapter typów. Oto jeden z takich przykładów.

// output:
//     Starting machine1
//     Stopping machine2

import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;

public class Foo
{
  // [{"machine_name":"machine1","command":"start"},{"machine_name":"machine2","command":"stop"}]
  static String jsonInput = "[{\"machine_name\":\"machine1\",\"command\":\"start\"},{\"machine_name\":\"machine2\",\"command\":\"stop\"}]";

  public static void main(String[] args)
  {
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
    CommandDeserializer deserializer = new CommandDeserializer("command");
    deserializer.registerCommand("start", Start.class);
    deserializer.registerCommand("stop", Stop.class);
    gsonBuilder.registerTypeAdapter(Command.class, deserializer);
    Gson gson = gsonBuilder.create();
    Command[] commands = gson.fromJson(jsonInput, Command[].class);
    for (Command command : commands)
    {
      command.execute();
    }
  }
}

class CommandDeserializer implements JsonDeserializer<Command>
{
  String commandElementName;
  Gson gson;
  Map<String, Class<? extends Command>> commandRegistry;

  CommandDeserializer(String commandElementName)
  {
    this.commandElementName = commandElementName;
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
    gson = gsonBuilder.create();
    commandRegistry = new HashMap<String, Class<? extends Command>>();
  }

  void registerCommand(String command, Class<? extends Command> commandInstanceClass)
  {
    commandRegistry.put(command, commandInstanceClass);
  }

  @Override
  public Command deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
      throws JsonParseException
  {
    try
    {
      JsonObject commandObject = json.getAsJsonObject();
      JsonElement commandTypeElement = commandObject.get(commandElementName);
      Class<? extends Command> commandInstanceClass = commandRegistry.get(commandTypeElement.getAsString());
      Command command = gson.fromJson(json, commandInstanceClass);
      return command;
    }
    catch (Exception e)
    {
      throw new RuntimeException(e);
    }
  }
}

abstract class Command
{
  String machineName;

  Command(String machineName)
  {
    this.machineName = machineName;
  }

  abstract void execute();
}

class Stop extends Command
{
  Stop(String machineName)
  {
    super(machineName);
  }

  void execute()
  {
    System.out.println("Stopping " + machineName);
  }
}

class Start extends Command
{
  Start(String machineName)
  {
    super(machineName);
  }

  void execute()
  {
    System.out.println("Starting " + machineName);
  }
}

Jeśli możesz zmienić interfejsy API, zwróć uwagę, że Jackson ma obecnie mechanizm stosunkowo prostej deserializacji polimorficznej. Opublikowałem kilka przykładów na programmerbruce.blogspot.com/2011/05/…
Programmer Bruce

RuntimeTypeAdapterjest teraz ukończony, niestety nie wygląda jeszcze tak, jakby był w rdzeniu Gson. :-(
Jonathan

8

Marcus Junius Brutus miał świetną odpowiedź (dzięki!). Aby rozszerzyć jego przykład, możesz uczynić jego klasę adaptera ogólną, aby działała dla wszystkich typów obiektów (nie tylko IAnimal) z następującymi zmianami:

class InheritanceAdapter<T> implements JsonSerializer<T>, JsonDeserializer<T>
{
....
    public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context)
....
    public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
....
}

A w klasie testowej:

public class Test {
    public static void main(String[] args) {
        ....
            builder.registerTypeAdapter(IAnimal.class, new InheritanceAdapter<IAnimal>());
        ....
}

1
Po wdrożeniu jego rozwiązania następną myślą było zrobienie dokładnie tego :-)
David Levy,

7

GSON ma tutaj całkiem niezły przypadek testowy pokazujący, jak zdefiniować i zarejestrować adapter hierarchii typów.

http://code.google.com/p/google-gson/source/browse/trunk/gson/src/test/java/com/google/gson/functional/TypeHierarchyAdapterTest.java?r=739

Aby tego użyć, wykonaj następujące czynności:

    gson = new GsonBuilder()
          .registerTypeAdapter(BaseQuestion.class, new BaseQuestionAdaptor())
          .create();

Metoda serializacji karty może być kaskadowa, jeśli-else sprawdź, jakiego typu jest ona serializowana.

    JsonElement result = new JsonObject();

    if (src instanceof SliderQuestion) {
        result = context.serialize(src, SliderQuestion.class);
    }
    else if (src instanceof TextQuestion) {
        result = context.serialize(src, TextQuestion.class);
    }
    else if (src instanceof ChoiceQuestion) {
        result = context.serialize(src, ChoiceQuestion.class);
    }

    return result;

Deserializacja jest trochę hakerska. W przykładzie testu jednostkowego sprawdza istnienie atrybutów ostrzegawczych, aby zdecydować, do której klasy należy dokonać deserializacji. Jeśli możesz zmienić źródło obiektu, który serializujesz, możesz dodać atrybut „classType” do każdej instancji, która zawiera FQN nazwy klasy instancji. Jest to jednak tak bardzo nie-obiektowe.


4

Firma Google wydała własną RuntimeTypeAdapterFactory do obsługi polimorfizmu, ale niestety nie jest to część rdzenia gson (musisz skopiować i wkleić klasę do projektu).

Przykład:

RuntimeTypeAdapterFactory<Animal> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory
.of(Animal.class, "type")
.registerSubtype(Dog.class, "dog")
.registerSubtype(Cat.class, "cat");

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(runtimeTypeAdapterFactory)
    .create();

Tutaj zamieściłem pełny działający przykład przy użyciu modeli Animal, Dog and Cat.

Myślę, że lepiej jest polegać na tym adapterze, niż wdrażać go od nowa.


2

Minęło dużo czasu, ale nie mogłem znaleźć naprawdę dobrego rozwiązania w Internecie. Oto mały zwrot w rozwiązaniu @ MarcusJuniusBrutus, który pozwala uniknąć nieskończonej rekurencji.

Zachowaj ten sam deserializator, ale usuń serializator -

public class IAnimalAdapter implements JsonDeSerializer<IAnimal> {
  private static final String CLASSNAME = "CLASSNAME";
  private static final String INSTANCE  = "INSTANCE";

  @Override
  public IAnimal deserialize(JsonElement json, Type typeOfT,
        JsonDeserializationContext context) throws JsonParseException  {
    JsonObject jsonObject =  json.getAsJsonObject();
    JsonPrimitive prim = (JsonPrimitive) jsonObject.get(CLASSNAME);
    String className = prim.getAsString();

    Class<?> klass = null;
    try {
        klass = Class.forName(className);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
        throw new JsonParseException(e.getMessage());
    }
    return context.deserialize(jsonObject.get(INSTANCE), klass);
  }
}

Następnie w swojej oryginalnej klasie dodaj pole z @SerializedName("CLASSNAME"). Sztuczka polega teraz na zainicjowaniu tego w konstruktorze klasy bazowej , więc przekształć swój interfejs w klasę abstrakcyjną.

public abstract class IAnimal {
  @SerializedName("CLASSNAME")
  public String className;

  public IAnimal(...) {
    ...
    className = this.getClass().getName();
  }
}

Powodem, dla którego nie ma tutaj nieskończonej rekurencji, jest to, że przekazujemy aktualną klasę środowiska wykonawczego (tj. Dog not IAnimal) do context.deserialize. To nie wywoła naszego adaptera typu, o ile używamy, registerTypeAdaptera nieregisterTypeHierarchyAdapter


2

Zaktualizowana odpowiedź - najlepsze części wszystkich innych odpowiedzi

Ja opisuję rozwiązań dla różnych przypadków użycia i będzie zajęcie się nieskończonej rekurencji problem jak dobrze

  • Przypadek 1: Jesteś w kontroli klas , czyli można dostać się do pisania własnych Cat, Dogzajęcia, jak również IAnimalinterfejs. Możesz po prostu zastosować rozwiązanie podane przez @ marcus-junius-brutus (najwyżej oceniana odpowiedź)

    Nie będzie nieskończonej rekurencji, jeśli istnieje wspólny interfejs podstawowy jako IAnimal

    Ale co, jeśli nie chcę implementować takiego IAnimallub innego interfejsu?

    Wtedy @ marcus-junius-brutus (najwyżej oceniona odpowiedź) spowoduje nieskończony błąd rekurencji. W takim przypadku możemy zrobić coś takiego jak poniżej.

    Musielibyśmy utworzyć konstruktor kopiujący wewnątrz klasy bazowej i podklasę opakowującą w następujący sposób:

.

// Base class(modified)
public class Cat implements IAnimal {

    public String name;

    public Cat(String name) {
        super();
        this.name = name;
    }
    // COPY CONSTRUCTOR
    public Cat(Cat cat) {
        this.name = cat.name;
    }

    @Override
    public String sound() {
        return name + " : \"meaow\"";
    };
}



    // The wrapper subclass for serialization
public class CatWrapper extends Cat{


    public CatWrapper(String name) {
        super(name);
    }

    public CatWrapper(Cat cat) {
        super(cat);
    }
}

I serializator dla typu Cat:

public class CatSerializer implements JsonSerializer<Cat> {

    @Override
    public JsonElement serialize(Cat src, Type typeOfSrc, JsonSerializationContext context) {

        // Essentially the same as the type Cat
        JsonElement catWrapped = context.serialize(new CatWrapper(src));

        // Here, we can customize the generated JSON from the wrapper as we want.
        // We can add a field, remove a field, etc.


        return modifyJSON(catWrapped);
    }

    private JsonElement modifyJSON(JsonElement base){
        // TODO: Modify something
        return base;
    }
}

Dlaczego więc konstruktor kopiujący?

Cóż, po zdefiniowaniu konstruktora kopiującego, bez względu na to, jak bardzo zmieni się klasa bazowa, opakowanie będzie nadal pełniło tę samą rolę. Po drugie, gdybyśmy nie zdefiniowali konstruktora kopiującego i po prostu podklasowali klasę bazową, musielibyśmy „rozmawiać” w kategoriach klasy rozszerzonej, tj CatWrapper. , . Jest całkiem możliwe, że komponenty mówią w kategoriach klasy bazowej, a nie typu opakowania.

Czy istnieje łatwa alternatywa?

Jasne, zostało wprowadzone przez Google - oto RuntimeTypeAdapterFactoryimplementacja:

RuntimeTypeAdapterFactory<Animal> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory
.of(Animal.class, "type")
.registerSubtype(Dog.class, "dog")
.registerSubtype(Cat.class, "cat");

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(runtimeTypeAdapterFactory)
    .create();

Tutaj musisz wprowadzić pole o nazwie „typ” Animali wartość tego samego wnętrza, Dogaby było „pies”, Cataby było „kotem”

Pełny przykład: https://static.javadoc.io/org.danilopianini/gson-extras/0.2.1/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.html

  • Przypadek 2: Nie kontrolujesz zajęć . Dołączasz do firmy lub korzystasz z biblioteki, w której klasy są już zdefiniowane, a Twój menedżer nie chce, abyś w jakikolwiek sposób je zmieniał - Możesz podklasować swoje klasy i zaimplementować wspólny interfejs znaczników (który nie ma żadnych metod ), takie jak AnimalInterface.

    Dawny:

.

// The class we are NOT allowed to modify

public class Dog implements IAnimal {

    public String name;
    public int ferocity;

    public Dog(String name, int ferocity) {
        super();
        this.name = name;
        this.ferocity = ferocity;
    }

    @Override
    public String sound() {
        return name + " : \"bark\" (ferocity level:" + ferocity + ")";
    }
}


// The marker interface

public interface AnimalInterface {
}

// The subclass for serialization

public class DogWrapper  extends Dog implements AnimalInterface{

    public DogWrapper(String name, int ferocity) {
        super(name, ferocity);
    }

}

// The subclass for serialization

public class CatWrapper extends Cat implements AnimalInterface{


    public CatWrapper(String name) {
        super(name);
    }
}

Więc używalibyśmy CatWrapperzamiast Cat, DogWrapperzamiast Dogi AlternativeAnimalAdapterzamiastIAnimalAdapter

// The only difference between `IAnimalAdapter` and `AlternativeAnimalAdapter` is that of the interface, i.e, `AnimalInterface` instead of `IAnimal`

public class AlternativeAnimalAdapter implements JsonSerializer<AnimalInterface>, JsonDeserializer<AnimalInterface> {

    private static final String CLASSNAME = "CLASSNAME";
    private static final String INSTANCE  = "INSTANCE";

    @Override
    public JsonElement serialize(AnimalInterface src, Type typeOfSrc,
                                 JsonSerializationContext context) {

        JsonObject retValue = new JsonObject();
        String className = src.getClass().getName();
        retValue.addProperty(CLASSNAME, className);
        JsonElement elem = context.serialize(src); 
        retValue.add(INSTANCE, elem);
        return retValue;
    }

    @Override
    public AnimalInterface deserialize(JsonElement json, Type typeOfT,
            JsonDeserializationContext context) throws JsonParseException  {
        JsonObject jsonObject = json.getAsJsonObject();
        JsonPrimitive prim = (JsonPrimitive) jsonObject.get(CLASSNAME);
        String className = prim.getAsString();

        Class<?> klass = null;
        try {
            klass = Class.forName(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new JsonParseException(e.getMessage());
        }
        return context.deserialize(jsonObject.get(INSTANCE), klass);
    }
}

Wykonujemy test:

public class Test {

    public static void main(String[] args) {

        // Note that we are using the extended classes instead of the base ones
        IAnimal animals[] = new IAnimal[]{new CatWrapper("Kitty"), new DogWrapper("Brutus", 5)};
        Gson gsonExt = null;
        {
            GsonBuilder builder = new GsonBuilder();
            builder.registerTypeAdapter(AnimalInterface.class, new AlternativeAnimalAdapter());
            gsonExt = builder.create();
        }
        for (IAnimal animal : animals) {
            String animalJson = gsonExt.toJson(animal, AnimalInterface.class);
            System.out.println("serialized with the custom serializer:" + animalJson);
            AnimalInterface animal2 = gsonExt.fromJson(animalJson, AnimalInterface.class);
        }
    }
}

Wynik:

serialized with the custom serializer:{"CLASSNAME":"com.examples_so.CatWrapper","INSTANCE":{"name":"Kitty"}}
serialized with the custom serializer:{"CLASSNAME":"com.examples_so.DogWrapper","INSTANCE":{"name":"Brutus","ferocity":5}}

1

Jeśli chcesz zarządzać TypeAdapter dla typu i innym dla jego podtypu, możesz użyć TypeAdapterFactory w następujący sposób:

public class InheritanceTypeAdapterFactory implements TypeAdapterFactory {

    private Map<Class<?>, TypeAdapter<?>> adapters = new LinkedHashMap<>();

    {
        adapters.put(Animal.class, new AnimalTypeAdapter());
        adapters.put(Dog.class, new DogTypeAdapter());
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
        TypeAdapter<T> typeAdapter = null;
        Class<?> currentType = Object.class;
        for (Class<?> type : adapters.keySet()) {
            if (type.isAssignableFrom(typeToken.getRawType())) {
                if (currentType.isAssignableFrom(type)) {
                    currentType = type;
                    typeAdapter = (TypeAdapter<T>)adapters.get(type);
                }
            }
        }
        return typeAdapter;
    }
}

Ta fabryka wyśle ​​najdokładniejszy TypeAdapter


0

Jeśli połączysz odpowiedź Marcusa Juniusa Brutusa z edycją user2242263, możesz uniknąć konieczności określania dużej hierarchii klas w swoim adapterze, definiując adapter jako pracujący na typie interfejsu. Następnie możesz podać domyślne implementacje toJSON () i fromJSON () w swoim interfejsie (który zawiera tylko te dwie metody) i mieć każdą klasę potrzebną do serializacji zaimplementuj swój interfejs. Aby poradzić sobie z rzutowaniem, w podklasach można zapewnić statyczną metodę fromJSON (), która deserializuje i wykonuje odpowiednie rzutowanie z poziomu interfejsu użytkownika. To zadziałało znakomicie dla mnie (po prostu uważaj na serializację / deserializację klas, które zawierają hashmapy - dodaj to podczas tworzenia wystąpienia swojego gson builder:

GsonBuilder builder = new GsonBuilder().enableComplexMapKeySerialization();

Mam nadzieję, że pomoże to komuś zaoszczędzić trochę czasu i wysiłku!

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.