'How to return custom response on javax.json.bind.JsonbException (Helidon project)

I'm trying to return a custom http response(400 bad request) instead of http 500 server error when json payload is not valid.
The problem is that when I use javax.json.bind.JsonbException as exception type in ExceptionMapper<T> it's not able to catch it, but I can catch the more generic javax.ws.rs.ProcessingException.
It is obvious that the javax.ws.rs.ProcessingException is caused by javax.json.bind.JsonbException as stack trace is showing:

javax.ws.rs.ProcessingException: Error deserializing object from entity stream.
    at org.glassfish.jersey.jsonb.internal.JsonBindingProvider.readFrom(JsonBindingProvider.java:86)
    at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$TerminalReaderInterceptor.invokeReadFrom(ReaderInterceptorExecutor.java:233)
    at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$TerminalReaderInterceptor.aroundReadFrom(ReaderInterceptorExecutor.java:212)
    at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor.proceed(ReaderInterceptorExecutor.java:132)
    at org.glassfish.jersey.server.internal.MappableExceptionWrapperInterceptor.aroundReadFrom(MappableExceptionWrapperInterceptor.java:49)
    at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor.proceed(ReaderInterceptorExecutor.java:132)
    at org.glassfish.jersey.message.internal.MessageBodyFactory.readFrom(MessageBodyFactory.java:1072)
    at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:885)
    at org.glassfish.jersey.server.ContainerRequest.readEntity(ContainerRequest.java:290)
    at org.glassfish.jersey.server.internal.inject.EntityParamValueParamProvider$EntityValueSupplier.apply(EntityParamValueParamProvider.java:73)
    at org.glassfish.jersey.server.internal.inject.EntityParamValueParamProvider$EntityValueSupplier.apply(EntityParamValueParamProvider.java:56)
    at org.glassfish.jersey.server.spi.internal.ParamValueFactoryWithSource.apply(ParamValueFactoryWithSource.java:50)
    at org.glassfish.jersey.server.spi.internal.ParameterValueHelper.getParameterValues(ParameterValueHelper.java:68)
    at org.glassfish.jersey.server.model.internal.JavaResourceMethodDispatcherProvider$AbstractMethodParamInvoker.getParamValues(JavaResourceMethodDispatcherProvider.java:109)
    at org.glassfish.jersey.server.model.internal.JavaResourceMethodDispatcherProvider$VoidOutInvoker.doDispatch(JavaResourceMethodDispatcherProvider.java:159)
    at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.dispatch(AbstractJavaResourceMethodDispatcher.java:79)
    at org.glassfish.jersey.server.model.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:475)
    at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:397)
    at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:81)
    at org.glassfish.jersey.server.ServerRuntime$1.run(ServerRuntime.java:255)
    at org.glassfish.jersey.internal.Errors$1.call(Errors.java:248)
    at org.glassfish.jersey.internal.Errors$1.call(Errors.java:244)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:292)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:274)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:244)
    at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:265)
    at org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:234)
    at org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:684)
    at io.helidon.webserver.jersey.JerseySupport$JerseyHandler.lambda$doAccept$4(JerseySupport.java:326)
    at io.helidon.common.context.Contexts.runInContext(Contexts.java:117)
    at io.helidon.common.context.ContextAwareExecutorImpl.lambda$wrap$7(ContextAwareExecutorImpl.java:154)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: javax.json.bind.JsonbException: Unable to deserialize property 'date' because of: Error parsing class java.util.Date from value: 2022-03-31T10:37:23.005Z[UTC]x. Check your @JsonbDateFormat has all time units for class java.util.Date type, or consider using org.eclipse.yasson.YassonConfig#ZERO_TIME_PARSE_DEFAULTING.
    at org.eclipse.yasson.internal.serializer.AbstractContainerDeserializer.deserializeInternal(AbstractContainerDeserializer.java:100)
    at org.eclipse.yasson.internal.serializer.AbstractContainerDeserializer.deserialize(AbstractContainerDeserializer.java:64)
    at org.eclipse.yasson.internal.Unmarshaller.deserializeItem(Unmarshaller.java:62)
    at org.eclipse.yasson.internal.Unmarshaller.deserialize(Unmarshaller.java:51)
    at org.eclipse.yasson.internal.JsonBinding.deserialize(JsonBinding.java:59)
    at org.eclipse.yasson.internal.JsonBinding.fromJson(JsonBinding.java:99)
    at org.glassfish.jersey.jsonb.internal.JsonBindingProvider.readFrom(JsonBindingProvider.java:84)
    ... 33 more
Caused by: javax.json.bind.JsonbException: Error parsing class java.util.Date from value: 2022-03-31T10:37:23.005Z[UTC]x. Check your @JsonbDateFormat has all time units for class java.util.Date type, or consider using org.eclipse.yasson.YassonConfig#ZERO_TIME_PARSE_DEFAULTING.
    at org.eclipse.yasson.internal.serializer.AbstractDateTimeDeserializer.deserialize(AbstractDateTimeDeserializer.java:74)
    at org.eclipse.yasson.internal.serializer.AbstractValueTypeDeserializer.deserialize(AbstractValueTypeDeserializer.java:64)
    at org.eclipse.yasson.internal.serializer.ObjectDeserializer.deserializeNext(ObjectDeserializer.java:180)
    at org.eclipse.yasson.internal.serializer.AbstractContainerDeserializer.deserializeInternal(AbstractContainerDeserializer.java:94)
    ... 39 more
Caused by: java.time.format.DateTimeParseException: Text '2022-03-31T10:37:23.005Z[UTC]x' could not be parsed, unparsed text found at index 29
    at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2055)
    at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1954)
    at java.base/java.time.ZonedDateTime.parse(ZonedDateTime.java:600)
    at org.eclipse.yasson.internal.serializer.DateTypeDeserializer.parseWithOrWithoutZone(DateTypeDeserializer.java:79)
    at org.eclipse.yasson.internal.serializer.DateTypeDeserializer.parseDefault(DateTypeDeserializer.java:49)
    at org.eclipse.yasson.internal.serializer.DateTypeDeserializer.parseDefault(DateTypeDeserializer.java:29)
    at org.eclipse.yasson.internal.serializer.AbstractDateTimeDeserializer.deserialize(AbstractDateTimeDeserializer.java:72)
    ... 42 more

But I was wondering if there is a way to catch the javax.json.bind.JsonbException?

I am using Helidon-MP 2.4.2.

Reproduce:

  1. create a quick start of Helidon-MP Link
  2. add a post method on greeting resource
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public void add(MyClass cal){
        System.out.println(cal.getDate());
    }
public class MyClass {
    private String name;
    private Date date;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public MyClass(String name, Date date) {
        this.name = name;
        this.date = date;
    }

    public MyClass() {
    }
    
}

3.send a post request with invalid json date. valid payoad:

{
    "name": "testg",
    "date": "2022-03-31T10:37:23.005Z[UTC]"
}

invalid (date) payload:

{
    "name": "testg",
    "date": "2022-03-31T10:37:23.005Z[UTC]XXXXXXXX"
}


Solution 1:[1]

I've been working in similar trouble, and i got this solution following this one previous solution (Jersey: Returning 400 error instead of 500 when given invalid request body).

import java.time.format.DateTimeParseException;
import javax.json.bind.JsonbException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import org.apache.johnzon.mapper.MapperException;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
@Provider
public class InvalidDataMapper implements ExceptionMapper<JsonbException>         {
@Override
public Response toResponse(JsonbException arg0) {
    if (arg0.getCause() instanceof MapperException) {
        MapperException exception = (MapperException) arg0.getCause();
        ErrorMessage errorMessage = null;
        if (exception.getCause() instanceof DateTimeParseException) {
            errorMessage = new ErrorMessage(CustomCodesAndMessages.CODE_ERROR_FORMAT_DATE, exception.getMessage());
        }
        if (exception.getCause() instanceof NumberFormatException) {
            errorMessage = new ErrorMessage(CustomCodesAndMessages.CODE_ERROR_FORMATO_NUMERIC, exception.getMessage());
        }
        return Response.status(Response.Status.BAD_REQUEST).type(APPLICATION_JSON_TYPE).entity(errorMessage).build();
    }
    return Response.status(Response.Status.BAD_REQUEST).build();
  }
}

And ErrorMessage is

package fif.tech.utiles;
import java.util.Objects;
import org.apache.cxf.jaxrs.ext.Nullable;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ErrorMessage {
private final int code;
private final String message;
private final String details;
public ErrorMessage(String message) {
    this(500, message);
}
public ErrorMessage(int code, String message) {
    this(code, message, null);
}
@JsonCreator
public ErrorMessage(@JsonProperty("code") int code,     @JsonProperty("message") String message,
                    @Nullable @JsonProperty("details") String details) {
    this.code = code;
    this.message = message;
    this.details = details;
}
@JsonProperty("code")
public Integer getCode() {
    return code;
}

@JsonProperty("message")
public String getMessage() {
    return message;
}

@JsonProperty("details")
@Nullable
public String getDetails() {
    return details;
}

@Override
public boolean equals(final Object obj) {
    if (this == obj) {
        return true;
    }
    if ((obj == null) || (getClass() != obj.getClass())) {
        return false;
    }

    final ErrorMessage other = (ErrorMessage) obj;
    return Objects.equals(code, other.code)
            && Objects.equals(message, other.message)
            && Objects.equals(details, other.details);
}

@Override
public int hashCode() {
    return Objects.hash(code, message, details);
}

@Override
public String toString() {
    return "ErrorMessage{code=" + code + ", message='" + message + "', details='" + details + "'}";
 }
}

Now i'm trying to catch the detailed field that produce the exception, but i think that is a enough solution.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 rfrp