Closed nimo23 closed 4 years ago
Maybe I got the issue wrong but sounds like you are missing a jaxrs routing provider (messagebody reader/writer delegating to jsonb or jackson depending the class). Jsonb can help to bridge annotation but not to support all jackson model (and the opposite as well)
you are missing a jaxrs routing provider
@rmannibucau Thanks for the hint. I guess, that is the reason. With jaxrs routing provider you mean something like resteasy
or jersey
?
The main application uses already resteasy
in combination with json-b
. But the 3rd party lib uses jackson
in combination with jersey
. Normally, the jaxrs routing provider of the 3rd party lib should be decoupled/isolated from the main application automatically, so each lib has its own dedicated routing provider (with its dedicated json processor) instead of inheriting the routing provider (and its dedicated json processor) of the main application. But it seems that this is not the case. Must I include a special library for that to work or can I configure json-b
to not propagate its routing provider to 3rd party lib (e.g. a config of something like isolateJaxrsThirdPartyImpl
)?
Normally, the jaxrs routing provider of the 3rd party lib should be decoupled/isolated from the main application automatically, so each lib has its own dedicated routing provider (with its dedicated json processor) instead of inheriting the routing provider (and its dedicated json processor) of the main application.
This is the case (side note: this is 100% in jaxrs) but when a provider is too much gluton it breaks the delegation chain. For JSON mapping this is hard to make it right since all mappers should know each others to make the right choice so it - by construction - ends up in user land to switch between them and I think it is the "least worse" solution, a framework solution would likely be a disaster with all possibilities and combinations we see in applications these days.
This is the case (side note: this is 100% in jaxrs)
Unfortunatley, this is not the case for me. I guess, because: there are two jaxrs-providers (jersey
and resteasy
coming from 3rd party lib) and two json-providers (jackson
and json-b
coming from my main application) in the same classpath. And both, the 3rd-party-lib and my main application use the same jax-rs annotations to (un)marshall classes - somehow there must be a conflict to delegate to its appropriate json processor because it delegates all json-processings to json-b
(yasson) and I dont know how to resolve that.
but when a provider is too much gluton it breaks the delegation chain.
I dont understand. Can you explain what "gluton" means?
ends up in user land to switch between them
And how can I switch between them within my application? How can I configure that json-b
does not touch the 3rd party class for json-processing. Is there a method or config-param where I can disable the delegation of jaxrs/json-processing to 3rd party libs? Or must I adapt my pom.xml
? How can I resolve this problem?
(on the phone so just giving pointers) long story short all happens in MessageBodyReader (and similarly Writer which is generally the same class in practise). Both read/write sides have a test to know if the impl (jsonb or jackson) must handle the class. Both are gluton in the sense if it is json it handles the class - and ignores if it should be the other provider. If your app is not consistent on serialization side you must provide a custom impl handling the switch between both (dont expect jackson to know all json mapper nor jsonb to handle jackson). So just write a custom @Provider. Alternatively you can make jackson or your jsonb impl (it is not portable yet) able to read the other impl annotations depending the api used.
To simplify the question the provider answer, it is equivalent to: being said the mimetype is json, should I handle class "Foo"? All providers will answer yes cause they actually handle all types (at least on write side)
@rmannibucau Thanks for helping!
have a test to know if the impl (jsonb or jackson) must handle the class
I tried it by adding this to my main application:
// the Task.class is a class defined within a 3rd-party lib which uses jackson annotations.
// I want that the jaxrs-routing-provider of my main application
// ignores the marshalling of Task.class
// to automatically delegate the marshalling
// to the jaxrs-routing-provider of the 3rd party lib
@Provider
public class IsolatedProvider implements MessageBodyReader<Task>{
@Override
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
// do NOT handle the 3rd-party class
// by the json provider of the main application
return false;
}
@Override
public ExchangeSymbol readFrom(Class<ExchangeSymbol> type,Type genericType, Annotation[] annotations,MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException {
return null;
}
}
However, the result is still the same: Json-B
(instead of jackson
) tries to marshall Task.class
. And even if I could manage that with the @Provider
solution, it would be very tedious to add a custom MessageBodyReader
for each 3rd-party class which my 3rd-party lib uses for marshallings. My 3rd-party lib has dozens of classes (not only the Task.class
) which is used for marshallings. Must I add one @Provider
for each 3rd-party class only to bypass the json-processing to jackson
?
Both are gluton in the sense if it is json it handles the class - and ignores if it should be the other provider.
What does the word "gluton" mean?
I also tried to solve this issue by adding explicitly only those classes which must be handled by the json-provider of my main application and ignores all other classes which are not listed:
@javax.ws.rs.ApplicationPath("")
public class ApplicationConfig extends Application {
@Override
public Map<String, Object> getProperties() {
var properties = new HashMap<String, Object>();
// does also not work
properties.put("resteasy.preferJacksonOverJsonB ", true);
return properties;
}
@Override
public Set<Class<?>> getClasses() {
// isolateJaxrsThirdPartyImpl
var classes = new HashSet<Class<?>>();
classes.add(MyResource.class);
return classes;
}
}
However, this solution does also not work. The json-provider of my main application uses json-b
for all json-processings and not only for MyResource.class
. I cannot find a solution.
Alternatively you can make jackson or your jsonb impl (it is not portable yet) able to read the other impl annotations depending the api used.
How can I configure this in yasson
?
By far the best solution would be:
The jaxrs routing provider of the main application should stop propagating the json processing provider to any 3rd-party lib. But I cannot find a solution for this. I tried it with custom MessageBodyReader
, but it does not work.
Few things to add in your impl for it to be used:
Something like this:
@Provider
@Priority(1)
@JBossLog
public class IsolatedProvider implements MessageBodyReader<Task> {
@Override
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
log.info(Arrays.asList(annotations));
return true;
}
@Override
public Task readFrom(
Class<Task> type,
Type genericType,
Annotation[] annotations,
MediaType mediaType,
MultivaluedMap<String, String> httpHeaders,
InputStream entityStream) throws IOException {
var hasJacksonAnnotation = Arrays.asList(annotations).stream().anyMatch(a -> a.getClass() == JsonCreator.class);
if (hasJacksonAnnotation) {
var mapper = new ObjectMapper();
try {
return mapper.readValue(entityStream, Task.class);
} catch (Exception e) {
log.error("Failed (toObject): ", e);
return null;
}
}
return null;
}
}
does not work. And even if this work, it is not fine, because with this I will override the jackson-mapper from the 3rd party library and use my own with totally different configurations of what the author of the 3rd party lib wanted. No nice.
this logic is generic for all classes
An implementation of MessageBodyReader
does not accept a generic type.
Unfortunately, I cannot find a solution to keep json-b
in my main application. It interferes if any 3rd party lib wants to use jackson
for marshalling. I guess, the only solution is to remove json-b
fully and use jackson
also for my main application. Unfortunately, with this I cannot use json-b
in the future anymore and just stick with jackson
(because it works seamlessly).
This is the case (side note: this is 100% in jaxrs) but when a provider is too much gluton it breaks the delegation chain.
This is as I described not case. I does still not know what you mean with "too much gluton". I dont know what you mean with the word "gluton".
No, MessageBodyReader
This is what we use in a few apps and used originally in geronimo microprofile openapi for example, it works well. Providers are just mapper selectors so you dont loose the config.
Side note: should likely move back to quarkus since it is not a jsonb issue ;)
This is what we use in a few apps and used originally in geronimo microprofile openapi for example, it works well.
Can you please give me a link with a working code? Anywhere in https://github.com/apache/geronimo-openapi? Or provide a generic implementation of MessageBodyReader
which delegates either to jackson or json-b?
Side note: should likely move back to quarkus since it is not a jsonb issue ;)
Yes, I thought it would be solvable by https://github.com/eclipse-ee4j/jsonb-api/issues/88. However, I think it is a different use case. Unfortunately, quarkus treats this issue as invalid https://github.com/quarkusio/quarkus/issues/5969.
package com.github.rmannibucau.jaxrs;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.annotation.Priority;
import javax.json.JsonStructure;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.ws.rs.Consumes;
import javax.ws.rs.Priorities;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.Providers;
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
@Provider
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Priority(Priorities.USER - 200)
public class JsonRoutingProvider<T> implements MessageBodyReader<T>, MessageBodyWriter<T> {
private volatile Jackson jackson;
private volatile JsonbJaxrsProvider jsonb;
private final ConcurrentMap<Type, JsonProvider> router = new ConcurrentHashMap<>();
@Context
private Providers providers;
@Override
public boolean isReadable(final Class<?> type, final Type genericType,
final Annotation[] annotations, final MediaType mediaType) {
init();
return jsonb.isReadable(type, genericType, annotations, mediaType) ||
jackson.isReadable(type, genericType, annotations, mediaType);
}
@Override
public T readFrom(final Class<T> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType, final MultivaluedMap<String, String> httpHeaders,
final InputStream entityStream) throws IOException, WebApplicationException {
init();
final JsonProvider provider = router.computeIfAbsent(genericType, t -> isJackson(t, new HashSet<>()) ? jackson : jsonb);
return (T) provider.readFrom(Class.class.cast(type), genericType, annotations, mediaType, httpHeaders, entityStream);
}
@Override
public boolean isWriteable(final Class<?> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType) {
init();
return jsonb.isWriteable(type, genericType, annotations, mediaType) ||
jackson.isWriteable(type, genericType, annotations, mediaType);
}
@Override
public long getSize(final T t, final Class<?> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType) {
return -1;
}
@Override
public void writeTo(final T t, final Class<?> type, final Type genericType,
final Annotation[] annotations, final MediaType mediaType,
final MultivaluedMap<String, Object> httpHeaders, final OutputStream entityStream) throws IOException, WebApplicationException {
init();
final JsonProvider provider = router.computeIfAbsent(genericType, key -> isJackson(key, new HashSet<>()) ? jackson : jsonb);
provider.writeTo(t, type, genericType, annotations, mediaType, httpHeaders, entityStream);
}
private void init() {
if (jsonb != null) {
return;
}
synchronized (this) {
if (jsonb == null) {
jsonb = new JsonbJaxrsProvider(providers);
jackson = new Jackson(providers);
}
}
}
private boolean isJackson(final Type genericType, final Set<Type> visited) {
if (!visited.add(genericType)) {
return false;
}
if (Class.class.isInstance(genericType)) {
final Class<?> clazz = Class.class.cast(genericType);
if (clazz.isArray() && clazz != clazz.getComponentType()) {
return isJackson(clazz.getComponentType(), visited);
}
return hasJacksonAnnotations(clazz);
}
if (ParameterizedType.class.isInstance(genericType)) {
final ParameterizedType pt = ParameterizedType.class.cast(genericType);
if (pt.getRawType() == Map.class) {
return pt.getActualTypeArguments().length == 2 && isJackson(pt.getActualTypeArguments()[1], visited);
}
if (Class.class.isInstance(pt.getRawType()) && Collection.class.isAssignableFrom(Class.class.cast(pt.getRawType()))) {
return pt.getActualTypeArguments().length == 1 && isJackson(pt.getActualTypeArguments()[0], visited);
}
return false;
}
return false;
}
private boolean hasJacksonAnnotations(final Class<?> clazz) {
return Stream.of(clazz)
.flatMap(c -> Stream.concat(Stream.concat(Stream.concat(
Stream.of(clazz.getAnnotations()),
Stream.of(clazz.getDeclaredFields())
.flatMap(f -> Stream.of(f.getAnnotations()))),
Stream.of(clazz.getDeclaredMethods())
.flatMap(f -> Stream.of(f.getAnnotations()))),
Stream.of(clazz.getDeclaredConstructors())
.flatMap(f -> Stream.of(f.getAnnotations()))))
.map(Annotation::annotationType)
.distinct()
.filter(a -> a.getName().startsWith("com.fasterxml.jackson."))
.findFirst()
.map(whateverMatches -> true)
.orElseGet(() ->
clazz.getSuperclass() != null &&
clazz.getSuperclass() != Object.class &&
hasJacksonAnnotations(clazz.getSuperclass()));
}
// just to share a common cacheable routed api and avoid 2 router maps
private interface JsonProvider extends MessageBodyWriter<Object>, MessageBodyReader<Object> {
}
private static class Jackson extends JacksonJsonProvider implements JsonProvider {
private Jackson(final Providers providers) {
this._providers = providers;
}
}
// simplified from johnzon
private static class JsonbJaxrsProvider implements JsonProvider, AutoCloseable {
private volatile Function<Class<?>, Jsonb> delegate = null;
private final Providers providers;
private JsonbJaxrsProvider(final Providers providers) {
this.providers = providers;
}
@Override
public boolean isReadable(final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType) {
return !InputStream.class.isAssignableFrom(type)
&& !Reader.class.isAssignableFrom(type)
&& !Response.class.isAssignableFrom(type)
&& !CharSequence.class.isAssignableFrom(type)
&& !JsonStructure.class.isAssignableFrom(type);
}
@Override
public boolean isWriteable(final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType) {
return !InputStream.class.isAssignableFrom(type)
&& !OutputStream.class.isAssignableFrom(type)
&& !Writer.class.isAssignableFrom(type)
&& !StreamingOutput.class.isAssignableFrom(type)
&& !CharSequence.class.isAssignableFrom(type)
&& !Response.class.isAssignableFrom(type)
&& !JsonStructure.class.isAssignableFrom(type);
}
@Override
public long getSize(final Object t, final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType) {
return -1;
}
@Override
public Object readFrom(final Class<Object> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType,
final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream) throws IOException, WebApplicationException {
return getJsonb(type).fromJson(entityStream, genericType);
}
@Override
public void writeTo(final Object t, final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType,
final MultivaluedMap<String, Object> httpHeaders, final OutputStream entityStream) throws IOException, WebApplicationException {
getJsonb(type).toJson(t, entityStream);
}
@Override
public synchronized void close() throws Exception {
if (AutoCloseable.class.isInstance(delegate)) {
AutoCloseable.class.cast(delegate).close();
}
}
private Jsonb getJsonb(final Class<?> type) {
if (delegate == null) {
synchronized (this) {
if (delegate == null) {
final ContextResolver<Jsonb> contextResolver = providers == null ?
null : providers.getContextResolver(Jsonb.class, MediaType.APPLICATION_JSON_TYPE);
if (contextResolver != null) {
delegate = new DynamicInstance(contextResolver); // faster than contextResolver::getContext
} else {
delegate = new ProvidedInstance(JsonbBuilder.create()); // don't recreate it
}
}
}
}
return delegate.apply(type);
}
private static final class DynamicInstance implements Function<Class<?>, Jsonb> {
private final ContextResolver<Jsonb> contextResolver;
private DynamicInstance(final ContextResolver<Jsonb> resolver) {
this.contextResolver = resolver;
}
@Override
public Jsonb apply(final Class<?> type) {
return contextResolver.getContext(type);
}
}
private static final class ProvidedInstance implements Function<Class<?>, Jsonb>, AutoCloseable {
private final Jsonb instance;
private ProvidedInstance(final Jsonb instance) {
this.instance = instance;
}
@Override
public Jsonb apply(final Class<?> aClass) {
return instance;
}
@Override
public void close() throws Exception {
instance.close();
}
}
}
}
this must work in any JAX-RS container
Thanks @rmannibucau I added this class to my application. The code makes sense and should normally differentiate between jsonb and jackson processing.
However, it does not work in quarkus
. I still get the same error as before because quarkus
processes jackson
annotated classes with jsonb
even if I registered JsonRoutingProvider
in my application. I think, it is a quarkus issue. Unfortunatley, @gsmet closed https://github.com/quarkusio/quarkus/issues/5969 with the reason:
The annotations are hints on how to serialize things but each serializer is perfectly capable of serializing without those hints.
As you see JsonRoutingProvider
makes the distinction of which json processing provider should be used solely by looking if class is annotated by jackson. Which makes absolutly sense! However, as long as quarkus treats annotations only as unnecessary hints, I think we cannot solve this issue. Because of this, I must drop the use of json-b
for future and use jackson
.
I also close this issue, because it is not a json-b
issue.
@rmannibucau Thanks for your time :)
According to https://github.com/eclipse-ee4j/jsonb-api/issues/88, we can adapt
json-b
annotation to 3rd party classes. Does this also mean that I do not have to use json-b annotations on classes to customize the json processing for each field/method because I can do this programmatically?Actually I have a big problem using
json-b
in quarkus. The problem is described in https://github.com/quarkusio/quarkus/issues/5969. And maybe https://github.com/eclipse-ee4j/jsonb-api/issues/88 would be a solution: My main app usesjson-b
-processing withjson-b
annotated classes. The problem is that I also use a 3rd party dependency which internally usesJackson
for json-processings. One one the 3rd party classes uses the following class to serialize with jackson:however, when this class is executed, unfortunately, json-b tries to serialize the class of the 3rd party dependency even though the 3rd party dependency should use its
jackson
for json processing. This results in an error forjson-b
becausejson-b
needs a no-arg-constructor which is missing:However, if
json-b
would ignore this class and delegate the json-processing of this class tojackson
(for what it was ment), then no error would occur, becausejackson
uses its@JsonCreator
. Actually, I cannot find a solution to make such a scenario working.So my question is: Can I disable the annotation processing or json processing with
json-b
for some classes with https://github.com/eclipse-ee4j/jsonb-api/issues/88 programmatically? I dont know whyjson-b
tries to serialize this class because this class was ment forjackson
-processing and not forjson-b
-processing. Is there any config to use injson-b
which would solve this issue? Or is https://github.com/eclipse-ee4j/jsonb-api/issues/88 the solution (e.g. programmatically disable thejson-b
-processing for some classes)?