jakartaee / jsonb-api

Jakarta JSON Binding
https://eclipse-ee4j.github.io/jsonb-api/
Other
78 stars 39 forks source link

Polymorphic [de]serialization #147

Closed mdzaebel closed 3 years ago

mdzaebel commented 5 years ago

I'd like to repeat the feature proposal from https://github.com/eclipse-ee4j/yasson/issues/133 here. There should at least be a description of how to implement Serializer/Deserializer that maintain runtime types. As there are more possible strategies, they should be defined too.

rmannibucau commented 5 years ago

+1, we have this kind of implementation in Apache Johnzon as well - it is portable: https://github.com/apache/johnzon/blob/master/johnzon-json-extras/src/test/java/org/apache/johnzon/jsonb/extras/polymorphism/PolymorphicTest.java#L84

mdzaebel commented 5 years ago

Yes, thats helpfull. Many implementations have a concepts with annotations or other strategies. I'd prefer also one without annotations, as this would enable a transformation without changing legacy classes. Also interesting, why did the expert group omit this feature in 1.0?

rmannibucau commented 5 years ago

@mdzaebel What is likely not possible is to make it transparent without any config (type=fully qualified name) so it requires to have explicit aliases for all children and whitelist the allowed children otherwise you get immediately a security issue. Therefore it is not strictly omitted in 1.0 since it is trivial to handle it in a (de)serializer. Hope it helps.

mdzaebel commented 5 years ago

At first, thanks so much for your help, which is really important for us (big insurance company). E.g. I did not know about the security aspect. I'm not an expert, so could you show me a serializer, that inserts a $type attribute and a deserializer that creates the instances out of $type? Currently, the security aspect could be solved by testing an instanceof relation in our case.

mdzaebel commented 5 years ago

May be JSON Binding 1.0 rather focus on the wrapping strategy, documented in https://javaee.github.io/javaee-spec/javadocs/javax/json/bind/serializer/JsonbDeserializer.html. So the usecase above seems to be difficult, but should be supported in future.

rmannibucau commented 5 years ago

@mdzaebel all the code is contained in this class: https://github.com/apache/johnzon/blob/master/johnzon-json-extras/src/main/java/org/apache/johnzon/jsonb/extras/polymorphism/Polymorphic.java - you can also import johnzon-extras module in your project and not fork it. Then just configure it as mentionned in previous comment (with the example link).

mdzaebel commented 5 years ago

Great stuff. That should be sufficient. Thank you Romain!

mdzaebel commented 5 years ago

With Yasson, your serializer doesn't work (Yasson Issue) e.g.

public void serialize(Dog obj, JsonGenerator generator, SerializationContext ctx) {
        ctx.serialize(new Wrapper(obj.getClass().getName(), obj), generator); // shortened
}

leads to "Recursive reference has been found in class ...", but isn't this correct, as the identical object has to be serialized in a deeper level recursively?

mdzaebel commented 5 years ago

Roman Grigoriadi now explained, that Yasson has to be used slighty different (see Issue)

Verdent commented 5 years ago

This feature is definitely a must to have so I thought about it and these are my suggestions which should be covered in terms of polymorphic (de)serialization.

  1. Global property Every serialized instance should have one additional property with the class name. This would be disabled by default and allowed to be configured or set by a property.

  2. Annotation based I have not yet come up with specific annotation name but this annotation could be added to the parent type of the classes user wants to (de)serialize. This will add new property to json with the class name of the instance. It should also have possibility to set "alias" name to each sub class and should not require to set exact class names in resulting json.

  3. Polymorphic configuration This would be part of the configuration which would allow user to add the same configuration possibility as in 2. even for classes one doesn't control or if he doesn't want to change existing classes.

It would also be possible to specify the name of the new property in each option mentioned above.

I would also suggest two levels of how strict it should be. For example aliases are set for some set of classes and during serialization is "not mentioned" class found.

What do you guys think about it? Any suggestions?

rmannibucau commented 5 years ago

@Verdent -10000 for your "1", this is exactly the 0-day vulnerability java serialization got and jackson is still trying to fix. 2 requires to use aliases - and it is not an option - known from the server (and not class names) for the same reason. Isn't 3 a custom deserializer - so already built-in?

Verdent commented 5 years ago

Yes I know about this. This would be just for those who want to use it internally. And there should be also warning that this might be potential security problem. But I wrote it here because it could be fairly simple to set up for a user.

In case of that 2. I would say that aliases are not that bad way to go. You have only several possible values and those are not invoked on the server. They are matched with corresponding class. What is wrong with that option? :-)

The 3. option was more or less 2. but in configuration way. You can create deserializer, but I was trying to find some way how to do it easier and more "automatic".

rmannibucau commented 5 years ago

My point on 2 was that only aliases are an option (being required and not just one option) since classname is a no go.

m0mus commented 5 years ago

@rmannibucau I agree that option 1 adds a vulnerability. But if it's switched off by default and users know that enabling it opens a security hole it's fine. Basic HTTP Authentication is an even bigger security hole, but still used. The same I can say for option 2. It's more secured, but opens a vulnerability in case alias is not specified. Again, users should know about it before enabling it. Do we have other options to make it more secured?

rmannibucau commented 5 years ago

We dont have to provide it at all, basic is still there cause it had been possible if you follow me. Off by default = on in prod (dont ask me how many spring dev actuators are in prod). What is limiting with aliases? Nothing except having the meta model we work in another issue to decorate external classes (a bit like cdi metamodel but lighter) so let's do it safe?

m0mus commented 5 years ago

What about defining a list of classes which we allow to serialize/deserialize in configuration. If JSONB tries to deserialize a class which is not in configuration it will throw an exception. It should solve the security issue, is it?

JsonbConfig config = new JsonbConfig()
    .serializable("com.test.model").
    .serializableAndDeserializable(com.test.SerializableClass);

// Create Jsonb with custom configuration
Jsonb jsonb = JsonbBuilder.create(config);

// Works fine
SerializableClass c = Jsonb.fromJson("some json", SerializableClass.class);

// Throws exception
NotSerializableClass c = Jsonb.fromJson("some json", NotSerializableClass.class);

// Works fine
Jsonb.toJson(com.test.model.Model);

// Throws exception
Jsonb.toJson(com.test.otherpackage.Model);
rmannibucau commented 5 years ago

toJson can pass but the fromJson is the one which must be controlled. Your solution works.

mdzaebel commented 5 years ago

@m0mus serializable(...) is an approach to handle security problems for polymorphic instantiation. It's only one aspect of it and should be defined within a polymorphic configuration. serializable(...) could be missunderstood as a general constraint, rather than a special one for polymorphic instantiation. I would drop duplicate methods like serializableAndDeserializable and use one method in both directions. Differences can always be defined with new JsonbConfig instances and should not occur often. So I would prefer a whitelisting approach like:

/** Polymorphic [de]serialization */
public JsonbConfig polymorphic(Polymorphic polymorphic);

Where Polymorphic could be defined like e.g.:

class Polymorphic {
   String typeAttribute="@type";
   Function<Class<?>, String>stringResolver; // String alias of typeAttribute 
   Function<String, Class<?>>classResolver;  // Deserialzation of alias value
   Strategy strategy;                        // How to serialize class types
   /** Classes/interfaces of which subclasses/implementations are handled:
     * Serialization: typeAttribute added.
     * Deserialization: Instantiation by typeAttribute */
   Class<?>[] classes;
}

This solves the security issue and the polymorphic configuration.

The proposals of @Verdent have to be decided. I would support them completely. It might be helpful to analyse the concepts already used by other implementations.

rmannibucau commented 5 years ago

jsonio used kinf of that and it failed in some of our project cause the lack of evolutivity in time (too rigid) + payloads where too big. Adapters/deserializers enable to be more clever and bypass type attributes IMHO since they can identify the type per attributes directly and not change the shape of the serialization which breaks js clients for example. So I'd prefer a lighter solution/abstraction if generic and not adapter based and not something against js. So at the end it is just providing a default serializer/adapter impl so let s keep it simple without new concept maybe? The whitelist is still needed though.

mdzaebel commented 5 years ago

Automatic type detection via attributes could also be a candidate for the "strategy". However, in many cases it's not sufficient, as attributes/values might be the same for different instance types. The concept has to be evolutive, thats an important point (what do you mean by "too rigid"?). E.g., if classnames change, it should be possible to use older and newer formats, which would emphasize the necessity of a strong alias concept. For internal communication, payload is not a problem but of course, JS-requirements have to be solvable too. Why does a type attribute break JS clients?

rmannibucau commented 5 years ago

@mdzaebel there is no interoperability standard at all (in the sense "commonly used standard") so if we change the shape of the payload then the clients will not consume it right (missing wrapper, custom attribute). It is likel producing a record following a schema without control. Therefore it must only be activable but never activated without a stringly explicit flag (annotation, no config probably).

mdzaebel commented 5 years ago

@rmannibucau Why no config? This would be more dynamic, while annotations are fixed at classes/methods/parameters. Each strategy has it's own settings, that have to be configurable by preferably one polymorphic configuration.

mkarg commented 5 years ago

@rmannibucau Just like Yasson, also Johnzon 1.1.13 fails at the same location:

public void serialize(Dog obj, JsonGenerator generator, SerializationContext ctx) { ctx.serialize(new Wrapper(obj.getClass().getName(), obj), generator); // shortened }

What actually happens is a Stack Overflow due to endless recursion:

Caused by: java.lang.StackOverflowError
    at org.apache.johnzon.mapper.MapperConfig.findObjectConverter(MapperConfig.java:186)
    at org.apache.johnzon.mapper.MapperConfig.findObjectConverterWriter(MapperConfig.java:175)
    at org.apache.johnzon.mapper.MappingGeneratorImpl.doWriteObject(MappingGeneratorImpl.java:153)
    at org.apache.johnzon.mapper.MappingGeneratorImpl.writeObject(MappingGeneratorImpl.java:111)
    at org.apache.johnzon.jsonb.serializer.JohnzonSerializationContext.serialize(JohnzonSerializationContext.java:41)
    at org.apache.johnzon.jsonb.extras.polymorphism.Polymorphic$Serializer.serialize(Polymorphic.java:64)
    at org.apache.johnzon.jsonb.JsonbAccessMode$WriterConverters.lambda$new$0(JsonbAccessMode.java:831)
    at org.apache.johnzon.mapper.MappingGeneratorImpl.doWriteObjectBody(MappingGeneratorImpl.java:306)
    at org.apache.johnzon.mapper.MappingGeneratorImpl.writeValue(MappingGeneratorImpl.java:463)
    at org.apache.johnzon.mapper.MappingGeneratorImpl.doWriteObjectBody(MappingGeneratorImpl.java:345)
    at org.apache.johnzon.mapper.MappingGeneratorImpl.doWriteObject(MappingGeneratorImpl.java:167)
    at org.apache.johnzon.mapper.MappingGeneratorImpl.writeObject(MappingGeneratorImpl.java:111)

Apparently even latest Johnzon tries to wrap again and again and again as soon once it serializes the wrapper itself but then sees the wrapper's payload and jumps into the polymorphic deserializer again. Romain, can you please decide whether this is a bug in Johnzon or simply a missing rule in the JSON-B spec?

rmannibucau commented 5 years ago

@mkarg it looks expected and normal with current spec - thought throwing a JsonbException("invalid") could have been saner. This code works using annotations and not global converters which leads to that. We likely don't want to skip serializer/deserializer/adapter in SerializationContext. If you set your impl globally (on jsonbconfig), it must likely have the standard threadlocal to prevent the loop if possible. That said, this use case does not fit serializer API which is really about how to serializer, it fits adapter API which is about changing the shape of a model then the new shape can have a serializer if you want I think.

mkarg commented 5 years ago

@rmannibucau I do not understand what you try to say, really! If I do not bind by annotations but by config, then I get the error message that only annotation binding is supported by Johnzon. So how to make this work actually? It is the original Johnzon Extras, it is the latest released Jonhzon, and it is exctly the example from the serializer. So Johnzon is actually not able to execute its own Extras?!

rmannibucau commented 5 years ago

@mkarg: wait, think topics are getting interlaced here so let me try to list the different points:

  1. johnzon polymorphic API is annotation oriented and is not intended to be used in JsonbConfig at all
  2. recursive serializer execution is intended (otherwise the outcome shape wouldn't be controlled anymore) but require the serializer to take care of the "loop". one solution is to use an adapter instead of a serializer to change the type before the serialization
mkarg commented 5 years ago

@rmannibucau Thank you for the clarification.

  1. It is written nowhere that Johnzon Extras Polymorphic API is not to be used with JsonbConfig at all. So you should document that clearly. Do I have to open a bug report for that?
  2. As that particular serializer used in my test is the one from Johnzon Extras your point 2. implies that it is a bug in Johnzon Extras' because that serializer does not take care of the loop, right? So I will file bug reports next.

BTW, how shall a serializer deal with that at all (it cannot guess which other serializers / deserializers have to be in place still, so all it could do is use Jsonb default config, which would be wrong is many cases)?

rmannibucau commented 5 years ago

@mkarg 1. usage is explicited here http://johnzon.apache.org/ (implicitly other usages are not intended but feel free to PR to clarify it), 2. no because of 1 ;).

Why don't you use an adapter?

mkarg commented 5 years ago

@rmannibucau

  1. As I strongly disagree that not showing a JsonConfig based use case implies other usages are not intended because reading the JSON-B spec mentiones no such limitations in any way, I will file a PR. Stay tuned.

  2. I do not understand "2. no because of 1" as the loop happens with the annotations-secenario (I do not use the JsonbConfig scenario). So why is the endless loop not a bug? I follow exactly the scenario described, using no explicit config at all but just the annotations!

I do not use an adapter because my current job is "replace our custom code by Johnzon Extras Polymorphic" and that means, no adapters but that serializer. My issue is not "how to make some polymorphism work" (I already have that) but "how to make Johnzon Extras Polymorphic not run into an endless loop while using no JsonbConfig but just annotations)!

rmannibucau commented 5 years ago

@mkarg this should likely move to johnzon user list but long story short the intended usage is to rely on annotations so if you don't then any unexpected behavior is actually not a bug. Without jsonbconfig global registration the polymorphic usage works - if not it is a bug we will fix but it is harnessed so should be very specific.

mkarg commented 5 years ago

@rmannibucau Agreed to switch over to Johnzon, as not general a JSON-B topic apparently. As I am relying on annotations and as I am not using JsonbConfig I will file an issue with Apache's JIRA. Thanks for all! :-) - See JOHNZON-273.

mkarg commented 5 years ago

@rmannibucau FYI

  1. I will file a PR. Stay tuned. See https://github.com/apache/johnzon/pull/43. :-)
mdzaebel commented 5 years ago

A difficult example of polymorphic [de]serialisation would be Throwable, as it is cyclic in getCause() by default, is a built-in class (so can't be annotated) and has a getter (e.g. getMessage()), that has different private field-name detailMessage. Furthermore, it has getStacktrace() with StackTraceElement[]'s, that have no noArgConstructor and a again Java built-in classes. Additionally, subclasses could have various custom payload fields.

rmannibucau commented 5 years ago

Cyclic serialization is handled separately - we have a flag for that in johnzon for instance. But it is transversal to polymorphism so we should keep it unrelated and orthogonal IMHO.

mdzaebel commented 5 years ago

Yes, of course, cyclic serialisation is a different issue. I just wanted to give a testcase that has the hardest problems in it. So, if we could de/serialize Exceptions polymorphically easily, this would be a big/important step for the next JSON-BIND version.

struberg commented 5 years ago

@mdzaebel we solve recursive references via writing a JsonPointer instead of the original value. When reading back the JSON we use the same Instance to which the JsonPointer pointer points to One of the restrictions right now is that you must use the same ordering strategy for writing and reading right now.

mdzaebel commented 5 years ago

Thanks for the tip.

m0mus commented 5 years ago

@mdzaebel My proposal about defining a scope for serialization and deserialization is wider than the polymorphism case. @Verdent proposal is fine, but we should allow using it only with scope definition. In this case we avoid security problems.

aguibert commented 5 years ago

To try and summarize here, at the core we need to have the following items:

  1. Some additional property added to the JSON which indicates the java class to be used
  2. To avoid security issues, the user must define the scope for the polymorphic deserialization. This could be either fully-qualified class names or entire packages.

As for what this code might look like, I think the proposal by @mdzaebel looks promising. I like the idea of offloading the detailed config to a separate class like this:

public JsonbConfig polymorphic(Polymorphic polymorphic);

If we keep adding more methods to JsonbConfig it will quickly become overwhelming for users because of too many methods.

Example scenario:

Suppose I have the following classes:

A user could configure polymorphism like this:

Polymorphic poly = Polymorphic.create()
  .withTypeAttribute("__type") // optional, defaults to something sensible
  .withClasses(Dog.class, Cat.class)
  .build();
Jsonb jsonb = JsonbBuilder.create(new JsonbConfig()
  .withPolymorphic(poly));

Dog dog = // ...
jsonb.toJson(dog);

The resulting JSON would be:

{
  "__type": "com.foo.Dog",
  "dogName": "Spot"
}

Then, it could easily be deserialized by another Java client using JSON-B.

However, not every service producing JSON will be Java obviously, and we shouldn't expect clients to know internal details about the Java application parsing the data. Namely, a JavaScript frontend shouldn't need to know that a Java backend has a class called com.foo.Dog, or even that it decided to use polymorphism to represent the JSON data.

To give a concrete example, suppose a JavaScript frontend has some other way of representing type information, perhaps:

{
  "animalType": "1", // suppose '1' means "Dog" for the frontend
  "dogName": "Spot",
  etc...
}

This is where the the proposed stringResolver and classResolver variables could come in handy:

Polymorphic poly = Polymorphic.create()
  .withClassResolver((animalType) -> {
    if ("1".equals(animalType)) return com.foo.Dog.class;
    if ("2".equals(animalType)) return com.foo.Cat.class;
    else // throw exception
  })
  // ...

Regarding @Verdent 's proposal, specifically item (2), I don't think an annotative approach is appropriate because in order to be secure, interfaces would have to declare implementations. (If impls declared their interfaces it could be a security issue) since scope could not be limited.

I don't like the way Yasson implemented this, which looks like this:

public class MyClass {
    @ImplementationClass(Dog.class)
    private Animal animal;
}

since the entire point of using an interface is defeated by needing to annotate the field/method with the implementation class.

rmannibucau commented 5 years ago

I'd keep JsonbConfig the entry point, rational being to keep a single entry point for end user which is generally appreciated. No issue for JsonbConfig to create a PolymorphicBuilder (likely just an interface and no implementation provided to let it be provider dependant as usual).

For consistency it should be a JsonbConfig#withPolymorphicConfig (and probably PolymorphicConfig[Builder] for the config instance) IMHO.

Finally the @ImplementationClass is not that nice except on List or Map<String, Animal> where it makes a lot of sense but should be a @JsonbPolymorphic(....Same config than the builder....) IMHO.

Hope it makes sense

aguibert commented 5 years ago

yep, we are in agreement

Verdent commented 5 years ago

I was asked by @m0mus to make some changes in my proposal and also make it with examples. This is a bit longer, but I think that it illustrates requested functionality pretty well.

Verdent commented 5 years ago

Yasson Polymorphism Support

Proposal

Create easy way to serialize and deserialize polymorphic java objects.

Yasson is framework for serialization and deserialization of java objects to json and back, but it still doesn't have automatic way to do it with polymorphic types. Currently when you want to (de)serialize this kind of classes, you need to create your own serializer/deserializer implementation to make it work.

This support could be allowed over

Functionality

Global polymorphic handling

Description

Every serialized instance should have one additional property with the class name. This would be disabled by default and allowed to be configured or set by a property.

When this feature is enabled it needs to have whitelist set up. Whitelist is set by configuration and allows user to define which classes can be deserialized by Yasson. This allows you to avoid potential security risk. Whitelist can be disabled, but it is recommended not to do that. If whitelist is disabled then warning should be printed out to the user.

Class name will be always serialized as the first property in each part of the json.

It is also possible to not set propertyName. If done so, it will be se serialized as json value without name.

Enable via configuration

Global polymorphism can be enabled directly via configuration properties

GlobalPolymorphicHandling gph = PolymorphicHandling.globalPolymorphicHandler()
                                                   .propertyName("someName")
                                                   .disableWhitelist()
                                                   .create();
WhitelistConfig whitelist = WhitelistConfig.builder()
                                           .packageName("some.package")
                                           .packageName("second.package")
                                           .type(SomeClass.class)
                                           .type(SecondClass.class)
                                           .build();

jsonbConfig.withGlobalPolymophism(gph);
jsonbConfig.withWhitelist(whitelist);

Example

Model

public class Car {
    public String name;
}
public class PersonalVehicle extends Car {
    public int maxSpeed;
}
public class Truck extends Car {
    public int size;
}

public class CarRental {
    public ArrayList<Car> carsForRent = new ArrayList<>();
}

Usage

GlobalPolymorphicHandling gph = PolymorphicHandling.globalPolymorphicHandler()
                                                   .propertyName("@type")
                                                   .create();
WhitelistConfig whitelist = WhitelistConfig.builder()
                                           .packageName("some.package")
                                           .type(ArrayList.class)
                                           .build();
JsonbConfig jsonbConfig = new JsonbConfig()
                                    .withGlobalPolymophism(gph)
                                    .withWhitelist(whitelist);

Jsonb jsonb = JsonbBuilder.create(jsonbConfig).build();

PersonalVehicle personal = new PersonalVehicle();
personal.name = "BMW";
personal.maxSpeed = 250;

Truck truck = new Truck();
truck.name = "MAN";
truck.size = 30;

CarRental carRental = new CarRental();
carRental.carsForRent.add(personal);
carRental.carsForRent.add(truck);

jsonb.toJson(carRental);

Output

{
    "carsForRent": {
        "@type":"java.util.ArrayList",
        "entries":[
            {
                "@type":"some.package.PersonalVehicle",
                "name":"BMW",
                "maxSpeed":250
            },
            {
                "@type":"some.package.Truck",
                "name":"MAN",
                "size":30
            }
        ]
    }
}

Output without propertyName set

{
    "carsForRent": [
        "java.util.ArrayList",
        [   
            [
                "some.package.PersonalVehicle",
                {
                    "name":"BMW",
                    "maxSpeed":250
                }
            ],
            [   "some.package.Truck",
                {
                    "name":"MAN",
                    "size":30
                }
            ]
        ]
    ]
}

Annotation based handling

Description

It is possible to enable polymorphic handling just for specific set of classes. This is enabled by adding @JsonbPolymorphicType annotation to the common parent of all classes you want to turn this on.

Another required annotation here is @JsonbSubType which allow us to define all possible child classes of this class and corresponding String alias to each of them. Note that alias names can't repeat. Each alias is defined in separate @JsonbSubType annotation. If non existing alias is found during deserialization an exception should be thrown.

It is also possible to set a little less secure setting here. If you want to (de)serialize also classes not mentioned in @JsonbSubType, you have to set className parameter at @JsonbPolymorphicType to true. This behavior is set by default to false and if enabled it should print warning to the console and also requires to have whitelist set up if not disabled.

Enable via annotation

@JsonbPolymorphicType
or
@JsonbPolymorphicType(property="@test", className = true)

@JsonbSubType(alias = "test", type = SomeClass.class)
@JsonbSubType(alias = "test2", type = SomeClassTwo.class)

Example

Model

@JsonbPolymorphicType(prefix="@annotType", className = true)
@JsonbSubType(alias = "personal", type = PersonalVehicle.class)
@JsonbSubType(alias = "truck", type = Truck.class)
public class Car {
    public String name;
}
public class PersonalVehicle extends Car {
    public int maxSpeed;
}
public class Truck extends Car {
    public int size;
}
public class MonsterTruck extends Car {
    public int wheelSize;
}

public class CarRental {
    public ArrayList<Car> carsForRent = new ArrayList<>();
}

Usage

WhitelistConfig whitelist = WhitelistConfig.builder()
                                           .packageName("some.package")
                                           .build();
JsonbConfig jsonbConfig = new JsonbConfig().withWhitelist(whitelist);
Jsonb jsonb = JsonbBuilder.create(jsonbConfig).build();

PersonalVehicle personal = new PersonalVehicle();
personal.name = "BMW";
personal.maxSpeed = 250;

Truck truck = new Truck();
truck.name = "MAN";
truck.size = 30;

MonsterTruck monsterTruck = new MonsterTruck();
truck.name = "MonsterTruck";
truck.wheelSize = 3;

CarRental carRental = new CarRental();
carRental.carsForRent.add(personal);
carRental.carsForRent.add(truck);
carRental.carsForRent.add(monsterTruck);

jsonb.toJson(carRental);

Output

{
    "carsForRent": [
        {
            "@annotType":"personal",
            "name":"BMW",
            "maxSpeed":250
        },
        {
            "@annotType":"truck",
            "name":"MAN",
            "size":30
        },
        {
            "@annotType":"some.package.MonsterTruck",
            "name":"MonsterTruck",
            "wheelSize":3
        }
    ]
}

Config based handling

Description

When we don't have control over some classes or we just don't want to make any changes to existing code, it is possible to add PolymorphicClassConfig to JsonbConfig. It has the same functionality as in case of annotation handling. It is also possible to handle classes which are not present as aliases but it requires to have it enabled by method enableClassNames. If enableClassNames is enabled, then it is required to have whitelist set up if not disabled.

Enable via configuration

PolymorphicClassConfig polymorphicConfig = PolymorphicHandling.classHandler(SomeClass.class)
                                                              .propertyName("someName")
                                                              .enableClassNames()
                                                              .disableWhitelist()
                                                              .addAlias("alias", Chield.class)
                                                              .create();
WhitelistConfig whitelist = WhitelistConfig.builder()
                                           .packageName("some.package")
                                           .build();

jsonbConfig.addPolymorphicClassHandling(polymorphicConfig);
jsonbConfig.withWhitelist(whitelist);

Example

Model

public class Car {
    public String name;
}
public class PersonalVehicle extends Car {
    public int maxSpeed;
}
public class Truck extends Car {
    public int size;
}
public class MonsterTruck extends Car {
    public int wheelSize;
}

public class CarRental {
    public ArrayList<Car> carsForRent = new ArrayList<>();
}

Usage

JsonbConfig jsonbConfig = new JsonbConfig();
PolymorphicClassConfig polymorphicConfig = PolymorphicHandling.classHandler(Car.class)
                                                              .propertyName("@type")
                                                              .enableClassNames()
                                                              .addAlias("personal", PersonalVehicle.class)
                                                              .addAlias("truck", Truck.class)
                                                              .create();
WhitelistConfig whitelist = WhitelistConfig.builder()
                                           .packageName("some.package")
                                           .build();

jsonbConfig.addPolymorphicClassHandling(polymorphicConfig);
jsonbConfig.withWhitelist(whitelist);

Jsonb jsonb = JsonbBuilder.create(jsonbConfig);
PersonalVehicle personal = new PersonalVehicle();
personal.name = "BMW";
personal.maxSpeed = 250;

Truck truck = new Truck();
truck.name = "MAN";
truck.size = 30;

MonsterTruck monsterTruck = new MonsterTruck();
truck.name = "MonsterTruck";
truck.wheelSize = 3;

CarRental carRental = new CarRental();
carRental.carsForRent.add(personal);
carRental.carsForRent.add(truck);
carRental.carsForRent.add(monsterTruck);

jsonb.toJson(carRental);

Output

{
    "carsForRent": [
        {
            "@type":"personal",
            "name":"BMW",
            "maxSpeed":250
        },
        {
            "@type":"truck",
            "name":"MAN",
            "size":30
        },
        {
            "@type":"some.package.MonsterTruck",
            "name":"MonsterTruck",
            "wheelSize":3
        }
    ]
}
mdzaebel commented 5 years ago

@Verdent Thanks for this elaborated proposal. I see following points:

May be we should start without annotation overriding as a configuration would enable everything we need?

mdzaebel commented 5 years ago

There is a special Collection case for java.util.Map. Maps because of type erasure at runtime. The Map keys/values should be [de]serialized polymorphically too. E.g. Jackson has problems with it and needs to be called with something like mapper.writerFor(new TypeReference<Map<Key, Value>>(){}).writeValueAsString(map)) where you need to know the types in advance. We should have an idea of how to cope with polymorphics map values.

rmannibucau commented 5 years ago

We use Type at jsonb to cope with it so no rela issue IMHO.

Verdent commented 5 years ago

@mdzaebel I definitely want to include also suggestions from @aguibert and @rmannibucau . I was working on this proposal with examples for a few days but I had it already ready when @aguibert post his proposal :-)

We should have a common concept of how to overwrite annotations programmatically ...

I do agree.

With your current proposal, it wouldn't e.g. be possible to make the handling compatible with Jackson, as Collection-Serialisation ...

I do not agree here. I think it would be possible. You would have to specify in config that polymorphic handling is only required for Set.class or Collection.class for example. In case of Object I think that it is exactly covered in one of my examples. Please point me to correct direction if that is not true :-)

The words "Handler" or "Handling" could be deleted. We should reduce the length of names if possible. ...

I do agree. That is fair point.

it could be advantageous to start with a simple predicate, that returns, whether a class should be polymorphic or not ...

I like this idea.

mdzaebel commented 5 years ago

@Verdent Thanks for your reply!

I do not agree here. I think it would be possible. You would have to specify in config that polymorphic handling is only required for Set.class or Collection.class for example. In case of Object I think that it is exactly covered in one of my examples. Please point me to correct direction if that is not true :-)

According to your proposal there are roughly two kinds of serialisation, with additional aliasing:

Whitelisting and aliasing is also possible. Jackson serializes by default arrays in the above Array-Type and objects in the Object-Type, where your approach seems to serialize only as either Object-Type or Array-Type (but not both), But may be, I missunderstood possible configurations? You said that one could configure Set/Collection as polymorphic, but this would rule out the necessary Objects? Hope I'm wrong?

Also, it's not clear to me, what results, if you deserialize a Map<?, Subtype> to Map<?,Object>. Will this result in a "Subtype" created (which we'd need)?

So if you agree to the other points, I'm curious about your revised proposal.

mdzaebel commented 5 years ago

Jackson deserializes objects according to their target type only. However, in secure environments, it could be advantageous to have an option, that simply creates the classes, that are given as the first type attribute. (jackson-user). This would enable the polymorphic transfer of highly generic data types, without the need to think about deserialization at all.

rmannibucau commented 5 years ago

So what is the concrete impact to the previous proposal? Support to parse generic types or change the @type enrichment from string to object?

Object already has rules in the spec and tcks (like numbers ends up as bigdecimal for example) so guess proposal is feature complete now, just need API review?