Search code examples
jooq

JOOQ: Getting an error "java.lang.IllegalArgumentException: ... is not an interface"


In my project I use PostgreSQL and JOOQ for all CRUD operations. Some data stored in JSON via JSONB data type. On the application side that data is represented by subclasses of an abstract class "com.fasterxml.jackson.databind.JsonNode".

As a described in official docs I have added my custom converter and binding to enable JsonNode support. Here some code:

import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Types;
import java.util.Objects;

import org.jooq.Binding;
import org.jooq.BindingGetResultSetContext;
import org.jooq.BindingGetSQLInputContext;
import org.jooq.BindingGetStatementContext;
import org.jooq.BindingRegisterContext;
import org.jooq.BindingSQLContext;
import org.jooq.BindingSetSQLOutputContext;
import org.jooq.BindingSetStatementContext;
import org.jooq.Converter;
import org.jooq.JSONB;
import org.jooq.conf.ParamType;
import org.jooq.impl.DSL;

import com.fasterxml.jackson.databind.JsonNode;

public class PostgresJSONJacksonJsonNodeBinding implements Binding<JSONB, JsonNode> {

    @Override
    public Converter<JSONB, JsonNode> converter() {
        return new PostgresJSONJacksonJsonNodeConverter();
    }

    @Override
    public void sql(BindingSQLContext<JsonNode> ctx) {
        if (ctx.render().paramType() == ParamType.INLINED) {
            ctx.render().visit(DSL.inline(ctx.convert(converter()).value())).sql("::jsonb");
        } else {
            ctx.render().sql(ctx.variable()).sql("::jsonb");
        }
    }

    @Override
    public void register(BindingRegisterContext<JsonNode> ctx) throws SQLException {
        ctx.statement().registerOutParameter(ctx.index(), Types.VARCHAR);
    }

    @Override
    public void set(BindingSetStatementContext<JsonNode> ctx) throws SQLException {
        ctx.statement().setString(ctx.index(), Objects.toString(ctx.convert(converter()).value(), null));
    }

    @Override
    public void get(BindingGetResultSetContext<JsonNode> ctx) throws SQLException {
        ctx.convert(converter()).value(org.jooq.JSONB.jsonb(ctx.resultSet().getString(ctx.index())));
    }

    @Override
    public void get(BindingGetStatementContext<JsonNode> ctx) throws SQLException {
        ctx.convert(converter()).value(org.jooq.JSONB.jsonb(ctx.statement().getString(ctx.index())));
    }

    @Override
    public void set(BindingSetSQLOutputContext<JsonNode> ctx) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }

    @Override
    public void get(BindingGetSQLInputContext<JsonNode> ctx) throws SQLException {
        throw new SQLFeatureNotSupportedException();
    }
}

import java.io.IOException;

import org.jooq.Converter;
import org.jooq.JSONB;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.NullNode;

public class PostgresJSONJacksonJsonNodeConverter implements Converter<JSONB, JsonNode> {

    private static final ObjectMapper OBJECT_MAPPER = JsonUtils.createObjectMapper();

    @Override
    public JsonNode from(JSONB dbObj) {
        try {
            return dbObj == null ? NullNode.instance : OBJECT_MAPPER.readTree(dbObj.data());
        } catch (IOException e) {
            throw new DataAccessException(e.getMessage(), e);
        }
    }

    @Override
    public JSONB to(JsonNode appObj) {
        try {
            return appObj == null || appObj.equals(NullNode.instance)
                ? null
                : JSONB.jsonb(OBJECT_MAPPER.writeValueAsString(appObj));
        } catch (IOException e) {
            throw new DataAccessException(e.getMessage(), e);
        }
    }

    @Override
    public Class<JSONB> fromType() {
        return JSONB.class;
    }

    @Override
    public Class<JsonNode> toType() {
        return JsonNode.class;
    }

}

I register these converter/binding on the JOOQ code generator and my generated model looks like:

public final TableField<RecordValueRecord, JsonNode> RCV_RECORD_VALUE = createField(DSL.name("rcv_record_value"), SQLDataType.JSONB.nullable(false), this, "...", new PostgresJSONJacksonJsonNodeBinding());

Then I try to select some values from DB. Here a simple fetch:

dslContext
    .select(RECORD_VALUE.RCV_RECORD_VALUE)
    .from(RECORD_VALUE)
    .where(...)
    .fetchInto(JsonNode.class)

In JOOQ 3.14.7 it works fine. JOOQ invokes my converter/binding and I get an instance of subclass of JsonNode.

But in 3.14.15 I get the error:

java.lang.IllegalArgumentException: com.fasterxml.jackson.databind.JsonNode is not an interface
  at java.base/java.lang.reflect.Proxy$ProxyBuilder.validateProxyInterfaces(Proxy.java:706)
  ...
  at org.jooq.impl.DefaultRecordMapper$ProxyMapper.proxy(DefaultRecordMapper.java:733)

The reason is here. JOOQ checks that JsonNode is an abstract class and creates ProxyMapper. My converter/binding is no longer being called and ProxyMapper crashes with an error.

What's wrong? What can i do to make it work?

Of course, explicitly specifying the converter helps, but I cannot specify it every time when working with JSON:

dslContext
    .select(RECORD_VALUE.RCV_RECORD_VALUE)
    .from(RECORD_VALUE)
    .where(...)
    .fetch(0, new PostgresJSONJacksonJsonNodeConverter());

Thanks!


Solution

  • I'm not sure if this is really a regression, or if it worked by accident before - I'm inclined to say it worked by accident.

    In any case, the ProxyMapper shouldn't apply for abstract classes, only for interfaces, so there's a bug here (it might not solve your problem, though): https://github.com/jOOQ/jOOQ/issues/13551

    There are various ways to fetch single column results into a List<T> in jOOQ. You chose one that isn't type safe and relies on the DefaultRecordMapper's reflection capabilities, which tend to have edge case behaviour like this one, for abstract classes. How about this, instead?

    List<JsonNode> list =
    dslContext
        .select(RECORD_VALUE.RCV_RECORD_VALUE)
        .from(RECORD_VALUE)
        .where(...)
        .fetch(r -> r.value1())
    

    Or this:

    List<JsonNode> list =
    dslContext.fetchValues(
         select(RECORD_VALUE.RCV_RECORD_VALUE)
        .from(RECORD_VALUE)
        .where(...)
    );