0

I'm having an issue returning data from one of my Quarkus REST endpoints. I've reviewed several similar questions here on SO (Jackson: No serializer found for class ~~~~~ and no properties discovered to create BeanSerializer, Quarkus reactive endpoint "no serializer found", Jackson serialize complicated class and occur the exception No serializer found for class). All of these describe my issue perfectly, but so far none of the solutions presented have worked.

This REST service has several endpoints. The intention of this project is to be a central data library for several micro-frontends we are working on. Therefore, I have an almost duplicate endpoint which works as expected.

I explain what I mean by "non-working" below

From my resource class, here are the two endpoint methods: Working

    @GET
    @Path("/all")
    @Produces(MediaType.APPLICATION_JSON)
    public Stream<Event> getAllEvents(){
        return service.getAllEvents();
    }

Non-Working

    @Path("/display")
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Stream<EmployeeOption> getOptionList(){
        try {
            return service.getOptionList();
        } catch (SQLException e) {
            throw new EventException("Unable to retrieve associate list",e);
        }
    }

Code from my repository classes Working

    public Stream<ActiveEvent> getActiveEvents() throws EventException{
        List<ActiveEvent> retVal = new ArrayList<>();
        try(Connection cn = eventsDs.getConnection();
            PreparedStatement ps = cn.prepareStatement(All_EVENTS)){

            ResultSet rs = ps.executeQuery();

            while(rs.next()){
                ActiveEvent item = new ActiveEvent();
                item.setEventId(rs.getString("eid"));
                item.setEventDescription(rs.getString("edescr"));

                retVal.add(item);
            }
        }catch (SQLException e){
            throw new EventException("Opps! Some unexplained, missed exception occurred", e);
        }

        return retVal.stream().sorted(Comparator.naturalOrder());
    }

Non-working

    public Stream<EmployeeOption> getEmployeeList() throws SQLException {
        List<EmployeeOption> retVal = new ArrayList<>();

        try (Connection cn = empDs.getConnection();
             PreparedStatement ps = cn.prepareStatement(DROPDOWN_SQL)){

            ResultSet rs = ps.executeQuery();

            while (rs.next()) {
                EmployeeOption e = new EmployeeOption();
                e.setUserId(rs.getString("user"));
                e.setDisplay(rs.getString("display"));
                retVal.add(e);
            }
        }

        return retVal.stream().sorted(EmployeeOption::compareTo);
    }

Model Classes Working

package com.goodyear.events.models;

import java.util.Objects;

public class ActiveEvent implements Comparable<ActiveEvent> {
    private String eventId;
    private String eventDescription;

    public String getEventId() {
        return eventId;
    }

    public void setEventId(String eventId) {
        this.eventId = eventId;
    }

    public String getEventDescription() {
        return eventDescription;
    }

    public void setEventDescription(String eventDescription) {
        this.eventDescription = eventDescription;
    }

    public int getActiveEventId(){
        return Integer.parseInt(eventId);
    }

    @Override
    public int compareTo(ActiveEvent o) {
        return Integer.parseInt(this.eventId) - Integer.parseInt(o.eventId);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof ActiveEvent)) {
            return false;
        }
        ActiveEvent ae = (ActiveEvent) o;
        return getActiveEventId() == ae.getActiveEventId();
    }

    @Override
    public int hashCode() {
        return Objects.hash(getActiveEventId());
    }
}

Non-working

package com.goodyear.events.models;

import io.quarkus.runtime.annotations.RegisterForReflection;

@RegisterForReflection
public class EmployeeOption implements Comparable<EmployeeOption> {
    private String userId;
    private String display;

    public String userId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String display() {
        return display;
    }

    public void setDisplay(String display) {
        this.display = display;
    }

    @Override
    public int compareTo(EmployeeOption o) {
        return userId.compareTo(o.userId);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof EmployeeOption)) {
            return false;
        }
        EmployeeOption that = (EmployeeOption) o;
        return userId.equals(that.userId);
    }

    @Override
    public int hashCode() {
        return 0;
    }
}

The service class simply passes the request on to the respective repository which will provide the data.

By non working I mean the endpoint method will either throw

2022-09-15 07:33:06,389 ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (executor-thread-0) HTTP Request to /data/display failed, error id: eb3d8291-c38b-4951-88d3-12c697b5da3b-2: org.jboss.resteasy.spi.UnhandledException: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.goodyear.events.models.EmployeeOption and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
    at org.jboss.resteasy.core.ExceptionHandler.handleException(ExceptionHandler.java:368)
    at org.jboss.resteasy.core.SynchronousDispatcher.writeException(SynchronousDispatcher.java:218)
    at org.jboss.resteasy.core.SynchronousDispatcher.writeResponse(SynchronousDispatcher.java:614)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:524)
    at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:261)
    at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:161)
    at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:364)
    at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:164)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:247)
    at io.quarkus.resteasy.runtime.standalone.RequestDispatcher.service(RequestDispatcher.java:73)
    at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler.dispatch(VertxRequestHandler.java:151)
    at io.quarkus.resteasy.runtime.standalone.VertxRequestHandler$1.run(VertxRequestHandler.java:91)
    at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:557)
    at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2449)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1478)
    at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
    at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.goodyear.events.models.EmployeeOption and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
    at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77)
    at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1300)
    at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400)
    at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.failForEmpty(UnknownSerializer.java:46)
    at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.serialize(UnknownSerializer.java:29)
    at com.fasterxml.jackson.databind.SerializerProvider.defaultSerializeValue(SerializerProvider.java:1142)
    at com.fasterxml.jackson.datatype.jdk8.StreamSerializer.lambda$serialize$0(StreamSerializer.java:74)
    at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
    at java.base/java.util.stream.SortedOps$SizedRefSortingSink.end(SortedOps.java:357)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
    at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
    at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.ReferencePipeline.forEachOrdered(ReferencePipeline.java:601)
    at com.fasterxml.jackson.datatype.jdk8.StreamSerializer.serialize(StreamSerializer.java:71)
    at com.fasterxml.jackson.datatype.jdk8.StreamSerializer.serialize(StreamSerializer.java:15)
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480)
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:400)
    at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1514)
    at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:1007)
    at org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider.writeTo(ResteasyJackson2Provider.java:345)
    at org.jboss.resteasy.core.messagebody.AsyncBufferedMessageBodyWriter.asyncWriteTo(AsyncBufferedMessageBodyWriter.java:24)
    at org.jboss.resteasy.core.interception.jaxrs.ServerWriterInterceptorContext.writeTo(ServerWriterInterceptorContext.java:87)
    at org.jboss.resteasy.core.interception.jaxrs.AbstractWriterInterceptorContext.asyncProceed(AbstractWriterInterceptorContext.java:203)
    at org.jboss.resteasy.core.interception.jaxrs.AbstractWriterInterceptorContext.getStarted(AbstractWriterInterceptorContext.java:166)
    at org.jboss.resteasy.core.interception.jaxrs.ServerWriterInterceptorContext.lambda$getStarted$0(ServerWriterInterceptorContext.java:73)
    at org.jboss.resteasy.core.interception.jaxrs.ServerWriterInterceptorContext.aroundWriteTo(ServerWriterInterceptorContext.java:93)
    at org.jboss.resteasy.core.interception.jaxrs.ServerWriterInterceptorContext.getStarted(ServerWriterInterceptorContext.java:73)
    at org.jboss.resteasy.core.ServerResponseWriter.lambda$writeNomapResponse$3(ServerResponseWriter.java:163)
    at org.jboss.resteasy.core.interception.jaxrs.ContainerResponseContextImpl.filter(ContainerResponseContextImpl.java:410)
    at org.jboss.resteasy.core.ServerResponseWriter.executeFilters(ServerResponseWriter.java:252)
    at org.jboss.resteasy.core.ServerResponseWriter.writeNomapResponse(ServerResponseWriter.java:101)
    at org.jboss.resteasy.core.ServerResponseWriter.writeNomapResponse(ServerResponseWriter.java:74)
    at org.jboss.resteasy.core.SynchronousDispatcher.writeResponse(SynchronousDispatcher.java:594)
    ... 16 more

Or, based on the recommended solution, will return an empty array. The service returns data to the endpoint method but then it is somehow erased when streamed back to the caller.

I simply do not see what the difference is that would cause this behavior.

Paul Stoner
  • 1,359
  • 21
  • 44

2 Answers2

1

Your non-working example class EmployeeOption has private fields, but no getters that follow getters' expected usual pattern getXYZ().

I'd suggest renaming your getters with names getDisplay() and getUserId().

peterhuba
  • 68
  • 1
  • 7
  • Also, make sure your getters are public (as they they are in this example.) – Bampfer Jan 13 '23 at 22:51
  • 1
    Good answer, helped me. Incidentally a way to avoid this getter problem is to implement DTOs like EmployeeOption using Java Records (introduced in Java 14). ``` record EmployeeOption (String userId, String display) {}``` The OP might still need that `compareTo` method, but they would get `equals` and `hashCode` implementations for free. – Bampfer Jan 13 '23 at 23:04
0

If you're building a Native image you may have forgotten to annotate your POJOs with @RegisterForReflection and based from the stack trace, seems like the culprit is EmployeeOption

When compiling for native image highly suggest reading the following resources, there is some extra stuff in there like the aforementioned and manually adding resources to a config. GraalVM generates a native image by static code analysis and only compiles and includes code that is reachable and you can't really perform an accurate analysis that uses reflective calls such as Jackson deserialization.