-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Apply JSON-B customizations to existing (i.e. 3rd party) classes #88
Comments
I think applying JSON-B metadata to objects that the developer is not able to modify is a good case to have covered. However, I would be strongly opposed to any sort of XML-based configuration, especially being as this is for a JSON standard. Ideally we can come up with some sort of java-based configuration mechanism in a future version of the spec. |
+1 for a java API, in johnzon we called it accessmode and it enables to map the instantiator, read and write properties. Guess jsonb can get the same in its builder. Another use case is to bridge an api, like supporting jaxb api to produce json like jettison does. |
I agree. We should provide an ability to configure JSONB in Java code. The functionality should cover all customizations/configuration provided by annotations. Also, JsonbConfig should support reading configuration from external sources using MicroProfile configuration or Config JSR which is in development now. |
To clarify: what I had in mind was JSON-B being able to read a JSON holding its configuration, so supporting any external tool should be as trivial as: jsonbConfig.withMappingConfiguration(myJsonConfig). myJsonConfig should be supported as being JsonObject, String or JsonbMapping (I assume we should be good enough to use ourself first). |
what would be the value of holding JSON-B config in JSON format? Could you include a sample of what you had in mind? If we need to do nesting in config, I agree JSON would be preferable. But IMO the simplest way to do config is just key/value pairs, and if config is just key/value pairs then JSON format is overkill. |
Class > field > annotations, a bit like bval does in xml. Key pairs is fine while it is set through a single property on jsonbconfig and not read by key in another backend |
Consider the following data model class: public static class Person {
public String name;
public LocalDate birthDate;
public LocalDate deathDate;
} We could apply In JSON, config might look like this: {
"com.example.Person" : {
"@JsonbDateFormat" : "##time-in-millis",
"properties" : {
"deathDate" : {
"@JsonbDateFormat" : "##default"
}
}
} But we could more concisely represent this information as key/value pairs like this:
|
Outch, why not just keeping it as it is in java? One object per annotation, one object per class etc... Also your concisely option breaks readability but also the one value point (keep in mind mp config will host multiple jsonb configs as today so you must use at least a prefix. Now i clearly prefer the first option which is mpconfig friendly and jsonbconfig friendly without any coupling vs second option which is coupled. Ps: dont forget jakata will likely be closer of jakata.config than microprofile since it should superseed it at the end so withMappingConfig(model) is good. Then json or properties is not very important, just a consistency point with the spec itself but properties are part of java.se so no big deal if using the common dot notation and not a specific slash one ;). |
I was chatting with my co-worker, @njr-11, on this and he raised a good argument that there shouldn't be any need to have externalized JSON-B config that can be overridden at deployment time. Really, all of the JSON-B configuration should be taken care of by the application developer who can either: |
How does B work for not managed classes? Frameworks use jsonb to store data in a database or so but the app server is not even aware of it. It would also be good to work in SE (A) otherwise adoption will not be there IMHO. That said I am also not super big fan of the config solution, it is not code driven. What about the model alternative, ie you can bind a class as being the model for another one, only the shape is taken into account:
This is close to jackson view for example. It does not require to invent a new config model. |
Yea I think you are on the right track and we are in agreement that a code-driven solution is the best option. A config-based solution would be very error-prone and confusing for users to learn about and therefore hard to adopt. The code snippet you proposed looks like it's on the right track, although I think the right Jackson feature to compare this to would be "Mix-ins" (see example here: https://medium.com/@shankar.ganesh.1234/jackson-mixin-a-simple-guide-to-a-powerful-feature-d984341dc9e2) |
Yep, mixed it with another framework calling views projections :s. Good catch. |
+1 for the Mixin configuration |
@m0mus can you elaborate on what you had in mind for this?
|
Why should we introduce our own configuration solution not compatible with everything else? We should support MicroProfile Config or Jakarta Config. Both projects support different configuration sources, formats, layering and other cool features. But the most important is that it will be a part of the standard. Something like this:
Sample of META-INF/microprofile-config.properties
|
@m0mus what does it bring compared to jsonbconfig.withFormatting(config.getValue(...)).withXxx(...) etc? Nothing exception a forced (and not desired in several cases) dependency. |
@rmannibucau The approach with reading individual properties from external configuration and set them to
I don't know anything about MP Config implementation details. You could be right and it uses JSONB itself. In this case implementations will need to think how to solve it. To be fair, I think that using Jakarta Config would be better because it's Jakarta. But I still don't know what's the progress and when Config JSR will be moved to Jakarta. And it may have the same JSONB dependency problem too. |
This is a big drawback for the apps I'm used to work with since it will just break the Jsonb instances. They don't share the same config and have several customizations. Even formatting=true would break a good part of apps (cause json is used by streams and a one line record is a requirement).
Ack but it is already achieved trivially so not really a pro of your solution IMHO.
This belongs to Microprofile (or Jakata and both standard will not use the same prefix, MP uses "mp.", jakata will likely keep "jsonb.") and they will clearly define the instances under their scope (JAX-RS @Provider, default app instance but not other instances or it would use the producer as prefix, like com.company.MyJsonbProducer.createJsonb) to ensure configuration is functional and fined grained enough to match all cases.
This is not MP-Config by itself but MP as a platform (when JSON-B is put with MP-Config typically). Individual specs must stay integrable but shouldn't be too tied otherwise the technology is already unusable in a lot of case and adoption is null from experience. Agree on your jakarta config point but this also justifies JSON-B can't be bound to MP IMHO. |
The discussion about integrating with some sort of MP/Jakarta Config spec is good, but I want to point out that it should be considered separately from this issue. I've raised #172 to track this The use case the OP was after is applying customizations to classes they cannot control, particularly things like adding By contrast, the MP/JEE Config integration could be useful to externalize default configurations for a MP/JEE Config should NOT be used to try and accomplish what the OP originally asked for. Such a solution would get very messy as shown in this comment: #88 (comment) |
Wanted to post an update on this issue, as I think it is one of the most important issues to address in JSON-B vNext. Proposed solution exampleConsider that a developer includes a class public class Dog {
public String name;
public int age;
public Person owner;
} Now suppose I want to customize this class without altering the source. For example, suppose I want to:
I think the simplest way to achieve this would be with the Jackson Mixin approach, which uses abstract classes that can be "merged" with the class being customized. For example: public class DogCustomization {
@JsonbProperty("dogName")
public String name;
@JsonbTransient
public Person owner;
// The "age" property is not mentioned here, so it is left as-is
} Then, the user could configure the customization for all instances of the Jsonb jsonb = JsonbBuilder.create(new JsonbConfig()
.withCustomization(Dog.class, DogCustomization.class)); Or, they could configure the customization at a per-property level like this: public class Foo {
@JsonbCustomization(DogCustomization.class)
public Dog dog;
} (Providing both approaches is similar to how Adapters, Serializers, and Deserializers can be configured globally or at a per-property level) Suggested new APIAdd method to the /**
* Register a customization for a class. A JSON-B customization class provides a way to
* apply JSON-B configurations, such as @JsonbTransient or @JsonbProperty, without
* modifying the source code of the original class. This is typically used when the original
* class cannot be modified, because it is included as a binary dependency.
* @param originalClass The class to apply the customization to
* @param customizationClass The abstract class containing the JSON-B customizations that
* will be applied to the originalClass
* @throws IllegalArgumentException If either of the supplied classes are null, or of customizationClass is not an abstract class
* @see @JsonbCustomization
*/
public JsonbConfig withCustomization(Class<?> originalClass, Class<?> customizationClass); New annotation: /**
* <same description as JsonbConfig#withCustomization>
*/
@JsonbAnnotation
@Retention(RetentionPolicy.RUNTIME)
@Target({METHOD, FIELD})
public @interface JsonbCustomization {
/**
* The abstract class containing the JSON-B customizations that
* will be applied to the type of the annotated field, or the return type
* of the annotated method.
*/
public Class<?> value();
} |
Hmm, few thoughts:
|
Valid question. While you can achieve this with an adapter, a lot of times writing a full adapter just to rename/ignore a handful of fields would be overkill. Consider the Dog example I used above. With an Adapter, it would look like this: public static class DogAdapter implements JsonbAdapter<Dog, Map<String, Object>> {
@Override
public Map<String, Object> adaptToJson(Dog obj) throws Exception {
Map<String, Object> dogMap = new HashMap<>();
dogMap.put("dogName", obj.name); // renamed property here
dogMap.put("age", obj.age); // unchanged
// ignore owner field
return dogMap;
}
@Override
public Dog adaptFromJson(Map<String, Object> obj) throws Exception {
Dog dog = new Dog();
dog.name = (String) obj.get("dogName"); // property name change
dog.age = (int) obj.get("age"); // default mapping here
// ignore owner field
return dog;
}
} This is quite a lot of code for a relatively small customization on a small class. In this example we are customizing 2 of 3 properties, but it would become even more tedious and error prone if the class had a large number of properties relative to the number of properties that wanted to be customized. For example, the class has 15 properties but I only want to customize 1 of them.
Again, it is possible to ignore third-party properties using a custom Visibility strategy, but it's clunky.
Yes, I think 2 customizations for the same class should be an error. What do we do today if you try to register 2 custom
Yes, ideally third party libraries would supply their own serializers. But often times there are odd cases where the third party lib would have no business creating a dependency on JSON-B API. For example in Yasson we ran into a case where generated Groovy classes needed to be ignored, and also Weld CDI proxy objects needed to be ignored. |
2-4. agree
|
I still think that the customization approach has value because it is flexible for any JSON-B annotation and gives the user what they want for this use case: the ability to annotate Classes they can't modify. In my original example I used renaming and ignoring properties, but lets lets consider a slightly different example where we want to apply a custom adapter to a property like this: public class DogCustomization {
public String name;
@JsonbAdapter(MyPersonAdapter.class)
public Person owner;
} I don't think you could easily represent this |
Well with an adapter you never hit this case or (previous Jsonb delegation) you can register it on the nested jsonb instance globally so no big deal. So overall it can be simpler but we must not compete with our existing API IMHO , this is why a custom annotation reader was a good option for me. |
I think that adapters and mix-ins can co-exist. |
Sure they can but will next feature reflection abstraction (we will not delay it very long after mixins since it is what it is built upon and allows to industrialize more user code) so maybe we can skip it? That's the main question, goal being to stay simple and not duplicate solutions for the same issue. |
Yes, but sometimes users don't want to register an adapter/serializer/deserializer globally, or they can't because they get a Jsonb instance from the container or something. I think the fact that we have a
Not sure what this "reflection abstraction" feature is. Do we have an issue open for it? Can you link to it?
In general I agree, but when one solution provides significant simplicity over an alternative, then it's OK. When I compare the usability of the proposed Customization approach to the existing Adapter approach, the Adapter approach seems like a hack/workaround. |
|
The reflection/visitor pattern you mention seems like it would be even more code overhead than the Adapter approach. For simple customizations like this I think the visitor pattern is overkill. This is why I prefer libraries like bytebuddy over ASM. |
For the "breaks strong typing" negative feedback, this is a valid concern but:
|
Try it, it is the same as visibility pattern and it is trivial to impl most of the time. At least way simpler than doing dozens of customization for an object graph. We dont need a recursive visitor pattern at all. Let say you want to ignore all "children" or "model" (loaded instance, not a config in some lib) attributes of external classes, you write as much mixins than models duplicating all attributes, for a lib iusing this pattern it is really overkill and long whereas an ModelAnnotationReader is 5 lines including class and method declarations and solves all cases smoothly + is naturally config friendly and dynamic friendly. |
Just to make sure we are on the same page, I would appreciate it if you posted a code example of what you have in mind so we can be sure we're discussing the same thing.
Yea I see what you mean here. If you have a class that has 20 properties on it and you want to ignore 19 of them, you'd have to model all 19 properties just for the sake of putting For example, suppose I have: public class ThirdPartyClass {
public String importantProp;
public String bogus1, bogus2, /* .... */ bogus19;
} If I want to only keep public class MyModel {
public String importantProp;
public MyModel(ThirdPartyClass obj) {
this.importantProp = obj.importantProp;
}
}
could you post a code example of what this might look like? Or provide a link to where this was previously discussed? |
@aguibert sure, something along https://github.com/apache/geronimo-openapi/blob/master/geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/processor/AnnotatedTypeElement.java, https://github.com/apache/geronimo-openapi/blob/master/geronimo-openapi-impl/src/main/java/org/apache/geronimo/microprofile/openapi/impl/processor/AnnotatedMethodElement.java + AnnotatedFieldElement returned by a AnnotatedModelReader { AnnotatedTypeElement read( ClassContext); AnnotatedMethodElement read(MethodContext); AnnotatedFieldElement read(FieldContext); } with the context being the underlying reflect instance and maybe some other metadata (probably the default impl to be able to reuse it as in SerializationContext). Note that methods can have defaults returning null or so to fallback on the default (current) impl. Your example is not exactly what I meant since it is exactly the good case for mixins but actually never happens (to ignore all but one property). My case was 20 classes, each of them having 1 property "X" you want to ignore. Here you need 20 mixins with potentially 20 properties redefinitions which is a lot just to ignore an internal model property. With the AnnotatedModelReader it is a blacklist impl which is very simple (you override read(FieldContext) and based on the field name return an annotated impl which adds JsonbTransient using JsonbTransient.Literal.INSTANCE or so (see https://javaee.github.io/javaee-spec/javadocs/javax/enterprise/inject/Default.Literal.html for that pattern). Does it makes more sense this way? |
@rmannibucau Reflection alternative approach you are suggesting is very powerful, but complicated. I prefer more simple user-friendly solutions. Before a user will get a full understanding how it works he will change his mind and use an alternative framework with more friendly approach to customizations. The amount of code has to be written is another drawback. I would prefer a solution requiring
I was thinking about adding a set of builders to JsonbConfig, something like:
or customization can be created somewhere else and shared between Jsonb instances this way:
Thoughts? |
Hi @m0mus, looks close to my proposal - while you enable a kind of .annotation(new MyCustomAnnotation()) builder method. I'd also add a readFrom method to be able to customize a mapping instead of creating it so:
whereas
or more likely to make it more friendly in the IDE and fluent:
This last option requires remove methods too:
Finally in term of API we must ensure we reuse JsonbProvider#newClassCustomization(Class) Side question: a detail but i'm not a fan of "customization", can we move to "model" semantic? ClassModel or ClassMapping maybe? |
I like the code example that @m0mus proposed, I think something along these lines would be a better way to solve the problem than what I originally suggested with the abstract class mixin approach. @rmannibucau I also like your idea of having a way to ignore source annotations. However, I think that two separate entry points ( // This example adds to/overrides source JSON-B annotations
ClassCustomization.builder(MyClass.class)
// ...
.build();
// This example would discard any source annotations for the entire class
ClassCustomization.builder(MyClass.class)
.ignoreSourceAnnotations()
// ...
.build();
// This example would discard any source annotations for
// properties "foo" and "bar"
ClassCustomization.builder(MyClass.class)
.ignoreSourceAnnotations("foo", "bar")
// ...
.build(); |
Makes sense for me - except naming point but my next question includes it. CDI has BeanConfigurator for that topic, do we want to align:
Wdyt? |
@rmannibucau I have no issues with having a method which reads customizations from a class. I agree with what @aguibert is proposing. It makes sense. |
Hi folks, I arrived at this issue after asking @aguibert about whether or not Mixins exist for JSON-B due to some folks complaining in Quarkus about various deserialization issues (which are not the fault of JSON-B). I initially had Jackson Mixins in mind, but I really like the proposal from @m0mus, it seems really elegant and as far as I can tell covers the use cases I had in mind (although I would like to see what a creator would look like). |
thanks for bringing up that case @geoand, I think we could model it in the same way the reflection API models obtaining a constructor with the public class MyClass {
public MyClass() {}
public MyClass(String s) {}
// Suppose we want to programmatically make this one the @JsonbCreator
public MyClass(String s, Foo f, int i)
} We could indicate which constructor we want to be the ClassCustomization.builder(MyClass.class)
.creator(String.class, Fool.class, int.class) // like @JsonbCreator
.build(); Also, we need to cover the case where a static creator method is used, such as: public class MyClass {
// want to put @JsonbCreator here
public static MyClass buildIt(String s) {
// custom initialization
}
} This case could be represented with an overloaded method such as: ClassCustomization.builder(MyClass.class)
.creator("buildIt", String.class) // like @JsonbCreator
.build(); In summary, we could have 2 methods on
Also, we could potentially add a 3rd method that would allow direct instantiation of a type using a Lambda:
|
@aguibert I like your proposal very much! |
Thanks, any thoughts on the last 2 options involving the lambdas? I think |
Honestly, I would probably just start without either of those for exactly the reason you mention - I wouldn't want to expose to much of the API without actually knowing that people need such a powerful solution for creating instances. |
There's a discussion about adding mixins (and more) as a general concept that can be used by other specs in the MP mailing list. Maybe we can join forces :-) |
Hi, I have added a new method An actual customization is based on builder pattern, and these builders are building set of different classes called "Decorators" since that's what we are doing, we are decorating types, fields, parameters etc. These decoraters contain all of the information passed to the builder. First exampleLet's assume we have following class public class Pojo {
public String myProperty;
public String getMyProperty() {
return myProperty;
}
public void setMyProperty(String myProperty) {
this.myProperty = myProperty;
}
} We cannot modify it, but we want to set custom name to "myCustomName" as we could do it easily with annotation @JsonProperty("myCustomName). We can do that easily like this TypeDecorator.builder(Pojo.class)
.property(PropertyDecorator.builder("myProperty")
.name("myCustomName")
.build())
.build(); Or you can also use shortcut method to do that TypeDecorator.builder(Pojo.class)
.property("myProperty", builder -> builder.name("myCustomName"))
.build(); This way you can apply every customization onto the component you are decorating with the same possibilities as with annotations. Second exampleExtend our Pojo class over the creator method public static Pojo create(String param1, Integer param2) {
return new Pojo(param1, param2);
} If we could apply annotations, creating creator would look similarly to this @JsonbCreator
public static Pojo create(@JsonbProperty("jsonName1") String param1,
@JsonbProperty("jsonName2") Integer param2) {
return new Pojo(param1, param2);
} But since we cannot do that, we can do it over the TypeDecorator TypeDecorator.builder(Pojo.class)
.creator(CreatorDecorator.builder("create")
.addParameter(String.class, "jsonName1")
.addParameter(Integer.class, "jsonName2")
.build())
.build(); Or again with shortcut method TypeDecorator.builder(Pojo.class)
.creator("create",
builder -> builder.addParameter(String.class, "jsonName1")
.addParameter(Integer.class, "jsonName2"))
.build(); It is also possible to set different customizations to the parameters since there is also separate builder to do so. ConclusionI think the whole feature designed like this is very user friendly and easy to use, since IDE will lead user with method completion, javadoc etc. Please, let me know what you think about it and if there is anything to change, what would that be and what would you propose. |
In the application, you may need to serialize/deserialize some objects which come from 3rd party library. So that we cannot put JSON-B annotaions on it.
For resolve this issue, just like some IoC framework, support XML configuration would be valuable. So, any plan for this kind of enhancement?
Thank you.
The text was updated successfully, but these errors were encountered: