jakartaee / jsonb-api

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

json-b tries to serialize jackson class #214

Closed nimo23 closed 4 years ago

nimo23 commented 4 years ago

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 uses json-b-processing with json-b annotated classes. The problem is that I also use a 3rd party dependency which internally uses Jackson for json-processings. One one the 3rd party classes uses the following class to serialize with jackson:

@JsonPropertyOrder({"id", "name", "date", "enabled"})
public class Task implements Serializable {

    private static final long serialVersionUID = -3670232159675112851L;

    private final String id;
    private final String name;
    private final LocalDate date;
    private final Boolean enabled;

    // no-arg constructor for json-b is missing,
    // but I cannot change this class because it is a 3rd party class !!
    // However, jackson does not need a no-arg-constructor when class uses @JsonCreator
    // Normally, this class should NOT be serialized by json-b but jackson
    @JsonCreator
    public Task(
            @JsonProperty("id") final String id,
            @JsonProperty("name") final String name,
            @JsonProperty("date") final LocalDate date,
            @JsonProperty("enabled") final Boolean enabled) {
        this.id = id;
        this.name = name;
        this.date = date;
        this.enabled = enabled;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public LocalDate getDate() {
        return date;
    }

    public Boolean getEnabled() {
        return isEnabled;
    }
}

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 for json-b because json-b needs a no-arg-constructor which is missing:

No default constructor found.
    at org.eclipse.yasson.internal.serializer.ObjectDeserializer.getInstance(ObjectDeserializer.java:95)
    at org.eclipse.yasson.internal.serializer.AbstractContainerDeserializer.deserialize(Abstr

However, if json-b would ignore this class and delegate the json-processing of this class to jackson (for what it was ment), then no error would occur, because jackson 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 withjson-b for some classes with https://github.com/eclipse-ee4j/jsonb-api/issues/88 programmatically? I dont know why json-b tries to serialize this class because this class was ment for jackson-processing and not for json-b-processing. Is there any config to use in json-b which would solve this issue? Or is https://github.com/eclipse-ee4j/jsonb-api/issues/88 the solution (e.g. programmatically disable the json-b-processing for some classes)?

rmannibucau commented 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)

nimo23 commented 4 years ago

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)?

rmannibucau commented 4 years ago

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.

nimo23 commented 4 years ago

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?

rmannibucau commented 4 years ago

(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)

nimo23 commented 4 years ago

@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.

rmannibucau commented 4 years ago

Few things to add in your impl for it to be used:

  1. @Priority to ensure it is called before built in ones (not sure if it is the case in jboss case, it is the default in cxf but not portable)
  2. IsXxx() method must return true, one impl is to visit the class and if some field use jackson then read with jackson else fallback on json, this logic is generic for all classes
  3. ReadFrom must be impl
nimo23 commented 4 years ago

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".

rmannibucau commented 4 years ago

No, MessageBodyReader, a generic one for all classes. It does not override 3rd party since you can inject the mapper from a ContextProvider in jaxrs. Also the else should use jsonb - looking up Jsonb from a context provider as well and creating it if null too.

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 ;)

nimo23 commented 4 years ago

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.

rmannibucau commented 4 years ago
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

nimo23 commented 4 years ago

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 :)