jakartaee / jsonb-api

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

Polymorphic [de]serialization #147

Closed mdzaebel closed 2 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.

mdzaebel commented 5 years ago

A possible first minimal solution could be:

public class JsonbConfig { ...
   /** Causes polymorphic serialisation/deserialisation with prepended typeAttribute,
       if the serialized type is more specific than its container type. Any specialisation,
       even Collection's are represented as {"typeAttribute": "qualified class name", ...}
       Unknown classes are deserialized as default of target value (later configurable)
       @return Newly created Polymorpic object for further polymorphic configuration */
   public Polymorphic polymorphic();
}
public class Polymorphic {
   /** Only classes are serialized, that are more specific and useClass is true. useClass 
       gets the qualified class name and the class (possibly null for deserialisation) */
   Polymorphic withClasses(BiPredicate<String, Class<?>> useClass);

   /** Sets the prepended typeAttribute for de/serialisation (default: "@class") */
   Polymorphic withTypeAttribute(String typeAttribute);

   /** The typeAttribute value is mapped to its de/serialization class */
   Polymorphic withAliases(Map<Object, Class<?>> aliasClass);

   /** Parent JsonbConfig object (or define a JsonConfig#create(Polymorphic builder) */
   JsonbConfig build();
}
Polymorphic polymorphic = new JsonbConfig().polymorphic()
      .withTypeAttribute("@type") 
      .withAliases(Map.of(1, C1.class, 2, C2.class)) // 1->C1, 2->C2 and reverse
      .withClasses((type, cls)->type.startsWith("com.my")); // only package polymorph
JsonbBuilder.create(polymorphic.build());
rmannibucau commented 5 years ago

A few remarks:

  1. WithClasses should likely be withPredicate or withFilter (semantically)
  2. Build method is not needed - as jsonbconfig it is not a builder
  3. I would use either just string for alias map or worse case a JsonValue as key but not an object (personally i would encourage string since other cases are not that needed IMHO and only use case i can see is fragile (like for enums, number must be used with caution and objects would break the simplicity and usage easiness of that model)
  4. On jsonbconfig it should be a withPolymorphic(Polymorphic) for consistency i guess

Next step is first to validate that proposal then do a pr with code update and spec update I think.

mdzaebel commented 5 years ago

Ok, with your propsed changes, we have:

public class JsonbConfig { ...
   /** Causes polymorphic serialisation/deserialisation with prepended typeAttribute,
       if the serialized type is more specific than its container type. Any specialisation,
       even Collection's are formatted as {"typeAttribute": "qualified class"|alias, ...}
       Unknown classes are deserialized as default of target value (later configurable)
       @return Newly created Polymorpic object for further polymorphic configuration */
   public Polymorphic withPolymorphic();
}
public class JsonbBuilder { ...   
    /** Creates Jsonb with polymorphic de/serialisation */
   public Jsonb create(Polymorphic builder);
}
public class Polymorphic {
   /** Only classes are serialized, that are more specific and useClass is true. useClass 
       gets the qualified class name and the class (possibly null for deserialisation) */
   Polymorphic withClasses(BiPredicate<String, Class<?>> useClass);

   /** Sets the prepended typeAttribute for de/serialisation (default: "@class") */
   Polymorphic withTypeAttribute(String typeAttribute);

   /** The typeAttribute value is mapped to its de/serialization class */
   Polymorphic withAliases(Map<JsonValue, Class<?>> aliasClass);
}
Polymorphic polymorphic = new JsonbConfig().withPolymorphic()
      .withTypeAttribute("@type") 
      .withAliases(Map.of(1, C1.class, 2, C2.class)) // 1->C1, 2->C2 and reverse
      .withClasses((type, cls)->type.startsWith("com.my")); // only package polymorph
JsonbBuilder.create(polymorphic);
rmannibucau commented 5 years ago

I will let others comment on it with my understanding was

public JsonbConfig withPolymorphicConfig(PolymorphicConfig p);

On aliases I understand but if you use stringified numbers it leads to the same, no? Perf will not be that impacted due to the rest of the chain work so I am not sure the gain to support multiple types, anything else you have in mind?

mdzaebel commented 5 years ago

Ok, latest adjustment (with a change for aliases to allow e.g. class.simpleName() as alias)

public class JsonbConfig { ...
   /** Causes polymorphic serialisation/deserialisation with prepended typeAttribute,
       if the serialized type is more specific than its container type. Any specialisation,
       even Collection's are formatted as {"typeAttribute": "qualified class"|alias, ...}
       Unknown classes are deserialized as default of target value (later configurable)
       @return Newly created PolymorpicConfig object for further configuration */
   public PolymorphicConfig withPolymorphicConfig();
}
public class JsonbBuilder { ...   
    /** Creates Jsonb with polymorphic de/serialisation */
   public Jsonb create(PolymorphicConfig builder);
}
public class PolymorphicConfig {
   /** Only classes are serialized, that are more specific and useClass is true. useClass 
       gets the qualified class name and the class (possibly null for deserialisation) */
   PolymorphicConfig withClasses(BiPredicate<String, Class<?>> useClass);

   /** Sets the prepended typeAttribute for de/serialisation (default: "@class") */
   PolymorphicConfig withTypeAttribute(String typeAttribute);

   /** The typeAttribute value is mapped to its de/serialization class */
   PolymorphicConfig withAliases(Function<Class<?>, String> classToAlias,
                                 Function<String, Class<?>> aliasToClass);
   PolymorphicConfig withAliases(Map<String, Class<?>> aliasClass)
}
PolymorphicConfig polymorphic = new JsonbConfig().withPolymorphicConfig()
      .withTypeAttribute("@type") 
      .withAliases(Map.of(1, C1.class, 2, C2.class)) // 1->C1, 2->C2 and reverse
      .withClasses((type, cls)->type.startsWith("com.my")); // only package polymorph
JsonbBuilder.create(polymorphic);

Next steps ?:

mdzaebel commented 5 years ago

Note, that with your proposal of withPolymorphicConfig(PolymorphicConfig) you would have to create the PolymorphicConfig with new, rather than fluently from JsonbConfig (more complex).

JsonbBuilder.create(new JsonbConfig().withPolymorphicConfig()); would get JsonbBuilder.create(new JsonbConfig().withPolymorphicConfig(new PolymorphicConfig())); which looks duplicate.

According to builder pattern, we wouldn't need "with" prefix or "Config" suffix but I don't mind to have them for consistency.

rmannibucau commented 5 years ago

@mdzaebel yep but at least it is consistent with JsonbConfig usage (so config graph is consistent). We can also forget about withPolymorphic(whateverargs) and just use setProperty(PolymorphicConfig.class.getName(), polymorphic).

I did the implementation and proposal in Johnzon: https://github.com/apache/johnzon/commit/56cd8957a2a9d36483eacc97911ff990403a5220. The Polymorphic config is available with its defaults at https://github.com/apache/johnzon/blob/56cd8957a2a9d36483eacc97911ff990403a5220/johnzon-jsonb/src/main/java/org/apache/johnzon/jsonb/api/experimental/PolymorphicConfig.java#L24, its usage with JsonbConfig is there https://github.com/apache/johnzon/commit/56cd8957a2a9d36483eacc97911ff990403a5220#diff-e20b40d7a5395c04f3dbb5f6d669e17aR51.

Note that the API is a bit different to match usage and ability to use it one way (deserialization or serialization). I also removed duplicate methods. Can you review that proposal please?

mdzaebel commented 5 years ago

It's a pleasure. Earliest in the evening.

mdzaebel commented 5 years ago

Just a first suggestion after flying over some implementation classes:

withDeserializationPredicate(final Predicate<Class<?>> deserializationPredicate) ...

should in my view get a String. Otherwise the class would have already been loaded, which is a security risk.

rmannibucau commented 5 years ago

@mdzaebel not there, let me explains more deeply this since it is a difference from your model. Here are the key points:

  1. you cant trigger polymorphic handling because the discriminator and an alias is present because this kind of hack can exist on precreated objects and not need the new logic
  2. the deserialization predicate is used on the static type in the model so it is already loaded anyway and not done on a dynamic class so it is safe. The "type loader" is then responsible to load the class from the alias.

Typially in my case Animal - an interface - will trigger the predicate and since this interface returns true it will trigger polymorphic deserialization with the discriminator logic.

mdzaebel commented 5 years ago

Romain, if you decide polymorphic deserialization on the target type, e.g. Objectwould raise trouble. The user usally needs to define, that certain interfaces/classes (say DataTransferObject's and Exception's) should be deserialised (nothing more). He owns this classes, so no security issue. If a polymorphic object resides in a container property e.g. of type Object, in your API we'd need to allow any class to be deserializable. This problem always occurs, if you have a superclass/interface with only a subset of valid subclasses. So we might need to have the class name to decide, whether the class should even be loaded. My useClass-method is planned to be called with class=null during deserialization.

Before analysing your implementation further, we should clarify, whether the above issue is relevant.

However, thanks a lot for the light speed implementation, that enables us, to be more specific on the necessary API. I'm open to any reasonable change, that preserves the necessary functionality.

rmannibucau commented 5 years ago

Agree on naming point, we can refine it but at least idea was to explicit what it does and the roles and not just have a symmetric naming (operation is actually not symmetric until you use a map as impl).

However on the predicate point, there is a single predicate but "per side" and both sides are different if you check out the usage and responsability. One side must handle impls, the other must handle abstraction so impl must be different IMHO or not sure what it would mean. It is fine for me to have a helper method set to both at once (setIncludePredicate or so).

Side note: final is not impacting end users so let's skip this point, i will not fight on that ;).

mdzaebel commented 5 years ago

So my revised API proposal (with all methods public) is:

public class JsonbConfig { ...
   /** Causes polymorphic serialisation/deserialisation with prepended typeAttribute,
       if the serialized type is more specific than its container type. Any specialisation,
       even Collection's are formatted as {"disciminator": "qualified class"|alias, ...}
       Unknown classes are deserialized as default of target value (later configurable) */
   PolymorphicConfig withPolymorphicConfig(PolymorphicConfig config);
}
public class PolymorphicConfig {
   /** Sets the prepended discriminator attribute for de/serialisation (default: "@class") */
   PolymorphicConfig withDiscriminator(String typeAttribute);

   /** Only classes are serialized, that are more specific and useClass is true. useClass 
       gets the class of the value to be serialized (poss. null) and the container type */
   PolymorphicConfig withSerializationPredicate(BiPredicate<Class<?>,Type> useClass);

   /** Only classes are deserialized/loaded of which useClass is true. useClass 
       gets the discriminator value and the dedicated target type */
   PolymorphicConfig withDeserializationPredicate(BiPredicate<String,Type> useClass);

   /** The object class and container type is mapped to its serialization alias String */
   PolymorphicConfig withSerializationAlias(BiFunction<Class<?>,Type,String> classToAlias);

   /** The discriminator value is mapped with its target type to the deserialization class */
   PolymorphicConfig withDeserializationAlias(BiFunction<String,Type,Class<?>> aliasToClass);

   /** Sets alias mapping for both directions */
   PolymorphicConfig withAliases(Map<String,Class<?>> aliasToClass)
}
// Usage example:
PolymorphicConfig polymorphic = new PolymorphicConfig()
      .withTypeAttribute("@type") 
      .withAliases(Map.of("1", C1.class, "2", C2.class)) // 1->C1, 2->C2 and reverse
      .withSerializationPredicate((c,t)->c==null ? null : c.getName().startsWith("com.my"));
JsonbBuilder.create(new JsonbConfig().withPolymorphicConfig(polymorphic));

Any method to be changed?

rmannibucau commented 5 years ago

Hmm, can you detail the bifunction usage? Think on serialization side it does not work. On deser side it works but means you allow the same alias for multiple type which should likely be not that easy/ecouraged IMHO.

On the jsonbconfig side i think we should just use properties to let it scale now ane not be modified for all new feature, what do other think?

mdzaebel commented 5 years ago

Hmm, can you detail the bifunction usage? Think on serialization side it does not work. On deser side it works but means you allow the same alias for multiple type which should likely be not that easy/ecouraged IMHO.

withSerializationAlias(BiFunction<Class<?>,Type,String> classToAlias), should get:

Why doesn't this work? Are these arguments not available?

withDeserializationAlias(BiFunction<String,Type,Class<?>> aliasToClass) should get:

This method just gets an other parameter, compared to your withDiscriminatorMapper. Why should more paramters allow same alias more than less parameters?

On the jsonbconfig side i think we should just use properties to let it scale now ane not be modified for all new feature, what do other think?

JsonbConfig is part of the API and could change too, if a large change is made in order to allow users to see and enable possible configurations easily.

rmannibucau commented 5 years ago

Point on jsonbconfig is nothing needs any specific method and it makes specific things which are not and keep forcing us to update it for any feature. It is saner for me to just stop adding aliases there to enable a single way to pass the config and to enable experimental extensions with the need of new api. Likely something to not do in this thread and handle in a dedicated one IMHO.

Oki, i get the bifunction usage. It however disable some optimizations and potential streaming impl to be portable - in particular with some other languages - and mixes two concerns (whitelisting and actual mapping). Personally I prefer to keep it distinct. Also, if you try to use method ref, it is more immediate if split and not combined (map::get vs a custom block with a switch for example).

mdzaebel commented 5 years ago
rmannibucau commented 5 years ago
mdzaebel commented 5 years ago

Romain, ok, JsonbConfig will be set by property.

I don't understand the second topic. I thought that withDeserializationAlias(BiFunction<String,Type,Class<?>> aliasToClass) would be ok for you? What is the impact exactly? Isn't both, the discriminator value and the target type easily available at deserialisation time?

rmannibucau commented 5 years ago

@mdzaebel Type is easily there since the java model is likely cached - can be assumed present at least, String (discriminator) is less obvious to be present. Think about the case you have a 1G payload - yes it happens ;), you don't want to load it all in mem just to check it is not handled - this is the implication of polymorphism to stay portable since the ordering of the payload is not guaranteed for portability reasons. Therefore ensuring it is 2 different functions is saner IMO. Also note that in terms of code style, having it split (both ways) is easier for end user cause you can use method reference instead of implementing a code block so I priviledge Function over BiFunction. Does it make more sense explained this way?

mdzaebel commented 5 years ago

If the problem is, that the discriminator value is difficult for bigger payload, because it can appear at any position, I thought it was absolutely clear, that the discriminator & value pair must be the first to appear in the list. So, we could get both values by checking, whether the first attribute is the disciminator and, if so, getting the discriminator value as next. If this does not help, what are the two functions, you would like to have?

rmannibucau commented 5 years ago

@mdzaebel if you assume the ordering, there is no need lf such a feature - (de)serializer is enough, only embracing the fact the ordering can't be guaranteed for interoperability reason justifies such a feature. If you assume the ordering then you break js integration for example which is still one common use case.

Side note: the other problem I see is the exposed api which is way more complex to impl than splitting it in two which enable to not own any logic for several simple cases as shown in the test.

mdzaebel commented 4 years ago

What if we use ["class", {...}] ? Wouldn't this preserve order?

rmannibucau commented 4 years ago

Yes but it breaks the shape which was considered the worst of polymorphic support earlier - and exactly a custom serializer case ;).

mdzaebel commented 4 years ago

So what methods do you propose and what are the drawbacks than?

rmannibucau commented 4 years ago

Ignoring the naming, I think johnzon experimental API model is not that bad, it can be enhanced with annotations - as proposed earlier - and enriched of a context (jsonpointer of the object) but this cost wouldnt be negligible for the whole graph so not sure it is worth it.

The main drawback is it breaks the interoperability of the model with other languages or libraries enforcing to use jsonb e2e if you dont respect original shape.

mdzaebel commented 4 years ago

In your experimental implementation, I tried jsonb.toJson(dog, Dog.class) which should not serialize with a discriminator. Hoverever, {"@type": "dog", "name": "wof"} results. In my view, withSerializationPredicate(Predicate<Class<?>>), should only be applied, if the class is already more special than it's target type.

rmannibucau commented 4 years ago

@mdzaebel not really, it must use the discriminator while the type matches, wherever it is. This is mainly due to the fact it is a global config (compare that to annotations which are contextual for example) and to ensure all serializations are consistent (think GET /animals vs GET /animal/1).

mdzaebel commented 4 years ago

For my company (and may be most others), unneccessary "@type" Attributes/Values are clearly not acceptable and could blow up JSON seriously. We not only need a possibility to configure it, but to have it as default.

rmannibucau commented 4 years ago

@mdzaebel well, as explained, what you want is not the proposal of that PR IMHO but a @PolymorphicConfiguration (whatever name, i'm just sharing the concept) to put a localizaion to the feature. That said if you do so you also breaks your clients most of the time for the explained reason so not sure it is that good to enable that + it is already doable using 2 Jsonb instances which is "ok" if you have such a custom need probably.

mdzaebel commented 4 years ago
rmannibucau commented 4 years ago

@mdzaebel :

  1. PR = pull request (was a language abuse for "issue" or "thread"), just get it as self referencing this discussion
  2. if you dont expose a consistent model (@type on all serialization of the same types) then the client does not know how to manipulate the instances. You can't assume the instance is only polymorphic locally. Take this example: class Cat implements Animal {String name;} and class Dog implements Animal {String name;}, without @type, the serialization of both classes will not be distinguishable at all and when you will expose these subtypes on an endpoint (likely GET /animal/{id}) then the client will not be able to handle it if you miss @type. The fact to add @type at the same level than first level attributes was to not break js clients, this enables to keep the same shape of the payload and interoperability stay at 100%. The fact to have the discriminator in one location but not in the other, enforces the server 1. to add the discriminator manually with another solution (like header or custom payload), 2.does not respect the global side of the configuration being done on JsonbConfig 3. prevents deserialization of the payload and enforces the client 4. to handle both objects differently (typically handleListResponse() vs handleItemResponse()). This is why I think that staying global gives the most to end users even if I can agree it can surprise you the first time.
  3. you can call JsonbBuilder.create(config) as much as you need in your app and you can change the config instance, then you just select the instance you want when you handle the instance, not sure the question is more specific but to give an example for jaxrs you can implement a ContextProvider which gives the Jsonb instance depending the class to serialize for example (simple facade pattern).
mdzaebel commented 4 years ago

The server could easily create Animals, for missing discriminator and dogs for existing disciminator, depending on the target type.

Your 4 arguments against this: 1: To have a default doesn't need anyone to add a discriminator anywhere 2: Of course, a default could be added by a global configuration 3: Of course, target-defaults don't disable deserialisation 4: List and Items are both classes and could well be created by default or specialized for an existing discriminator

rmannibucau commented 4 years ago

Not sure I get the default point, when you get {name:foo} you can fallback on Dog.class but if it was a cat you broke the round trip so default can only makes sense in terms of migration (v1 were all dogs and v2 have cats now) which is likely more a preprocessing of the JsonObject than a mapping feature to me.

mdzaebel commented 4 years ago

May be I didn't make clear, what I meant with default. I mean, that the default is the target type. E.g. if the server expects an Animal (a class rather than an interface), than is should create the Animal, if the discriminator is missing. So in your example, {"name": "foo"} should become an animal with the name "foo". If {"@type":"Dog", "name": "foo"} is sent, it should be a Dog, if the mapper is configured as Class::simpleName. If Animal is an interface, I will never have to serialize it, but allways more special classes. So, it is sufficient to say, that the discriminator must only appear, if the instance class is not the same as the target class.

rmannibucau commented 4 years ago

Hmm, still means you don't have generic handlers or (corrollar) you are not able to validate it works since you will silently convert the json without @type to an instance which is maybe not (POST {name} -> mapAnimal(Dog.class) will work even if @type should have been cat if set).

I now get where you want to go but I think that for the first time it happens in a release we should stick to something simpler, ie global or not cause it has a lot of weird cases where not having it will make the software not robust at all.

mdzaebel commented 4 years ago

Ok, what about making this aspect configurable, via:

/** Whether same classes get discriminator values. Default: true (I'd like false ;-) */
public PolymorphicConfig withDiscriminatorSame(boolean);
rmannibucau commented 4 years ago

This would mean withPolymorphicConfig().withoutPolimorphism() so I dont think it would be good.

mdzaebel commented 4 years ago

So you define polymorphism as sending vast amounts of unneccessary information? E.g. Jackson, polymorphically serializes BigInteger[] [1] as

["[Ljava.math.BigInteger;", [["java.math.BigInteger", 1]]]

This is clearly not acceptable with respect to length and readability. If you know, you have a BigInteger[] as a target type, [1] is sufficient (3 chars, rather than 59).

Unfortunately, I really don't understand many of your arguments, because you use such short and general expressions like "you break your client" or "has a lot of weird cases" or "will make the software not robust" without explanation.

We use JSON for remote communication in an internal environment (intranet). May be, there are different requirements, in a public API, that you might have in mind?

In my view, it's trivial to add some lines to Yasson (ObjectSerializer ...) so I'll go this way, as it seems, that there are not many people interested in this topic. Many companies don't use inheritance in their DTO's but we do for historical reasons. Additionally, I can't convince you, so we will not even agree on an API proposal. It's unclear to me, who is working with what effort on these topics in the API team. So doing it myself is the only chance.

However, you helped me a lot and answered me all my questions so patiently. May be, if we'd have the chance to meet at any time, it would be much easier, to understand our positions.

So thanks a lot and best luck for a solid and helpful API design.

rmannibucau commented 4 years ago

Polymorphism for a (de)serializer is the unambiguity of the serialized version. This definition implies the type is represented in the serialization or provided all the time. Since jsonb does not support a schema - compared to avro gor instance - it must be in the serialization version. Your request is a particular case where you want to enable the polymorphism only for a particular jsonpointer of a type. I never said it is not useful but it matches jsonb annotation api and not global one which is what this issue is about.

Hope it makes more sense.

amoscatelli commented 4 years ago

Any progress about this ? I created the feature request in the first place. My company also uses polymorphism in DTO's and still I can't switch to JsonB API.

Also, take notice that OpenAPI 3.0 supports polymorphism now (via discriminatory field). I really think this is a MUST HAVE feature.

amoscatelli commented 4 years ago

May be I didn't make clear, what I meant with default. I mean, that the default is the target type. E.g. if the server expects an Animal (a class rather than an interface), than is should create the Animal, if the discriminator is missing. So in your example, {"name": "foo"} should become an animal with the name "foo". If {"@type":"Dog", "name": "foo"} is sent, it should be a Dog, if the mapper is configured as Class::simpleName. If Animal is an interface, I will never have to serialize it, but allways more special classes. So, it is sufficient to say, that the discriminator must only appear, if the instance class is not the same as the target class.

Yea this is exactly as I expected it to be. I think this is efficient and clear.

mdzaebel commented 4 years ago

Yes, target type default should at least be an option and I proposed a configuration switch for it. Thanks for the notice about OpenAPI 3.0.

benneq commented 4 years ago

Our company is also using polymorphism in DTOs. We always use a single string as discriminator, sometimes within the object itself, sometimes within its parent:

{ "@type": "foo", "value": "bar" }
vs.
{ "@type": "foo", "data": { "value": "bar" } }

(No real technical reason. It's mainly because we couldn't find any meaningful pro/con arguments for or against each variant.)

We never automatically use class names as discriminator. It's always an explicitly defined custom string - even if it's often the same as the class name. Automatically using class names is of course simple, but then renaming / refactoring can break things easily. Basically we only need a Map<Class, String> (and reverse for deserialization) that includes all polymorphic types. But I guess there are other projects that need more flexibility.

hartimcwildfly commented 4 years ago

Has anybody got a working example for this? The example here https://javaee.github.io/jsonb-spec/users-guide.html does not work. Because when using a List< Animal > JsonB uses the serializer for Cat/Dog instead of the one for Animal. (Seems legit to me but the example does not work)

Found a solution for this. If anybody is interested I will upload a minimal example.

bmarwell commented 4 years ago

Has anybody got a working example for this? The example here https://javaee.github.io/jsonb-spec/users-guide.html does not work. Because when using a List< Animal > JsonB uses the serializer for Cat/Dog instead of the one for Animal. (Seems legit to me but the example does not work)

ping @aguibert -- see comment above

aguibert commented 4 years ago

hi @hartimcwildfly, the link you posted is no longer active since it is for Java EE. Instead you should reference the Jakarta EE site here: http://json-b.net/docs/user-guide.html#serializersdeserializers

We corrected this specific example in the doc to not do polymorphic deserialization because it can lead to infinite recursion / stackoverflow if not done correctly.

bmarwell commented 4 years ago

From the original post:

Implement similar @SubTypes annotation in Javax package like it is in Jackson.

and from march 30: https://github.com/eclipse-ee4j/jsonb-api/issues/147#issuecomment-605907731

Any progress about this ? I created the feature request in the first place.

I am also following that feature request for quite some time now and would like to see it implemented. I tried to find a discussion on gitter (there is a gitter chatroom eclipse/jsonb), but it seems kind of empty.

amoscatelli commented 3 years ago

I still can't understand why we can't proceed with this ...

The current situation is simply wrong. Without polymorphism support, JsonB serializations and deserializations aren't inverse functions, unlike any serialization-deserialization function pair is supposed to be. Jsonb isn't just about Json. It's about Json AND JAVA. Java is a STRONGLY TYPED language. In Java, type information is preserved at runtime, therefore a Dog !== Cat even if they have the same attributes! Therefore, the type has to be explicitly reported, possibly without focusing on the specific technicality.

The issue here is not about HOW we should serialize the type information: the issue here is to provide proper and generic means to do such a thing.

This is what I propose:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface JsonbPolymorphic {
    Class<? extends JsonbPolymorphismHandler> handler() default JsonbPolymorphismHandler.class;
}

public interface JsonbPolymorphismHandler {
    public JsonObjectBuilder writeType(JsonObjectBuilder builder, Class<?> type);
    public Class<?> readType(JsonStructure json);
    public JsonStructure readValue(JsonStructure json);
}

Serialization:

Before the end of the serialization of an instance of a class annotated (inherited or not) with JsonbPolymorphic, all the serialized attributes should be put into a JsonObjectBuilder (NOT a JsonObject ! the build() method is not called yet); then, Jsonb will ask CDI for an instance of a type annotated with @JsonbPolymorphismHandler and, if present, will call the writeType() method, passing the JsonObjectBuilder and the current runtime object type as parameters. If more beans are elegible, CDI shall throw an exception as usual.

So, for example, it will call writeType() passing as parameters:

The specific JsonbPolymorphismHandler (developed by the user, by now, since JsonB did not define a standard to specify the type and no de-facto standard can be inferred from actual usage) will handle the type serialization manipulating the same JsonObjectBuilder or returning a new one. This will allow the programmer to have ANY resulting Json structure, according to its needs:

EITHER

{
     "__type": $typeinformation
     "attribute1": ...
     ..
     "attributeN": ...
}

OR

{
    $typeinformation: {
        "attribute1": ...
        ..
        "attributeN": ...
    }
}

OR anything that comes into your mind.

This will be flexible enough, isn't it ?

Deserialization :

At the beginning of deserialization of the JSON payload into a class annotated with @JsonbPolymorphic, when JSONP turns the json payload into a JsonStructure, Jsonb will ask CDI for an instance of a type annotated with @JsonbPolymorphismHandler and, if present, will call both readType() and readValue() methods, passing the JsonStructure as parameter to determine the type and the real JsonStructure to parse. This will allow support for flexible type serialization, as already discussed. If more beans are elegible, CDI shall throw an exception as usual.

At this point, the normal deserialization of the JsonStructure (retuned by the readValue() method) to the type returned by the readType() method will occour.

I stress out the fact the this abstraction allows the optional use of any polymorphism management and mapping between serialized information and the actual type. Being optional, it means this won't impact any mechanism already in production, neither on the server nor on the client side.

@rmannibucau Please let me know if this is ok for you. I believe JSONB is going to be get more and more importance and polymoprhism is a key feature. JSONB is already used by NoSql upcoming implementations to serialize beans to nosql databases.

I can make a pull request for the spec and help with Yasson implementation too, if you allow me to proceed. What is important is to get this done.

rmannibucau commented 3 years ago

Hi

Isnt your api proposal a duplicate of serializer/deserializer api gor a particular case? Looks like it with this handler form (vs a built in @JsonbTypeId). It is already supported IMHO.