Jakarta EE integration for jjq — provides Hibernate persistence types, JPA AttributeConverter,
JAX-RS providers, Jakarta JSON-B serializers, and direct @JqMapped record mapping that
bypasses Jackson entirely for annotated record types.
<dependency>
<groupId>io.hyperfoil.tools</groupId>
<artifactId>jjq-jakarta</artifactId>
<version>${jjq.version}</version>
</dependency>The module depends on hibernate-core, jakarta.ws.rs-api, jakarta.json.bind-api, and
jakarta.json-api with provided scope — your application's runtime (Quarkus, Spring, WildFly)
supplies the actual versions. Only include the features you use; unused Jakarta APIs are not
required at runtime.
Two approaches for persisting JqValue fields:
Zero-copy persistence using direct byte serialization and JDBC binary streaming. Deferred string values from byte parsing are copied as raw bytes during serialization — no String construction, no UTF-8 re-encoding, no intermediate array copies.
The @JqValueColumn composite annotation combines the three Hibernate annotations
(@JdbcType, @JavaType, @Mutability) into one:
import io.hyperfoil.tools.jjq.jakarta.JqValueColumn;
import io.hyperfoil.tools.jjq.value.JqValue;
@Entity
public class MyEntity {
@JqValueColumn
@Column(columnDefinition = "BYTEA")
public JqValue data;
}@Column(columnDefinition = "BYTEA") is still needed separately because column definitions
are database-specific (BYTEA for PostgreSQL, BLOB for SQLite/MySQL, etc.).
If you prefer explicit annotations, the equivalent expanded form is:
@Column(columnDefinition = "BYTEA")
@org.hibernate.annotations.JdbcType(JqValueJdbcType.class)
@org.hibernate.annotations.JavaType(JqValueJavaType.class)
@Mutability(Immutability.class)
public JqValue data;Write path (zero copy):
JqValues.serializeToByteOutput(value)— serializes directly into a pre-sizedbyte[]buffer (noArrays.copyOf)PreparedStatement.setBinaryStream(index, stream, length)— wraps the buffer inByteArrayInputStream(no copy). PostgreSQL's JDBC driver stores the stream reference and reads lazily during execute.
Read path:
ResultSet.getBytes()→JqValues.parse(byte[])— SWAR-optimized byte parser with field name interning
Benefits:
- 2LC compatible:
JqValueimplementsSerializablewith proper singleton preservation - No FormatMapper registration needed —
@JqValueColumnis self-contained - No JSONB overhead — avoids PostgreSQL's JSONB parse/validate on write and JSONB-to-text serialize on read
- Immutability-safe —
JqValueis immutable, so Hibernate skipsdeepCopy()for snapshots and second-level cache. Dirty checking uses.equals(), so reassigning the field to a differentJqValueinstance is correctly detected
Migration from JSONB: If switching an existing JSONB column to BYTEA:
ALTER TABLE my_entity ALTER COLUMN data TYPE BYTEA USING data::bytea;Uses String-based I/O via Hibernate's FormatMapper SPI. Suitable when you need
PostgreSQL JSONB operators or GIN indexes on the column.
@Entity
public class MyEntity {
@Column(columnDefinition = "JSONB")
@JdbcTypeCode(SqlTypes.JSON)
@Mutability(Immutability.class)
public JqValue data;
}The FormatMapper requires registration with your framework:
Quarkus:
@ApplicationScoped
@PersistenceUnitExtension
@JsonFormat
public class MyJsonFormatMapper extends JqValueFormatMapper {}Spring Boot:
@Configuration
public class HibernateConfig {
@Bean
public JqValueFormatMapper jqValueFormatMapper() {
return new JqValueFormatMapper();
}
}Plain Hibernate:
hibernate.type.json_format_mapper=io.hyperfoil.tools.jjq.jakarta.JqValueFormatMapperFor portable JPA persistence that works with any JPA provider (Hibernate, EclipseLink,
OpenJPA), use the JqValueConverter:
import io.hyperfoil.tools.jjq.jakarta.JqValueConverter;
@Entity
public class MyEntity {
@Convert(converter = JqValueConverter.class)
@Column(columnDefinition = "TEXT")
private JqValue metadata;
}Each field requires an explicit @Convert annotation. Auto-apply is intentionally
disabled to avoid conflicting with the Hibernate-specific @JdbcType/@JavaType
BYTEA mapping on other JqValue fields in the same entity.
- Write:
JqValue.toJsonString()-> VARCHAR/TEXT column - Read:
JqValues.parse(String)->JqValue nulldatabase values map to Javanull
For better performance on PostgreSQL, prefer the Hibernate-specific JqValueJdbcType (BYTEA)
or JqValueFormatMapper (JSONB) described above.
The module includes @Provider-annotated MessageBodyReader and MessageBodyWriter
implementations that handle JqValue serialization for REST endpoints.
These are auto-discovered by JAX-RS implementations (Quarkus RESTEasy, Jersey, etc.)
via the @Provider annotation. No additional configuration is needed.
import io.hyperfoil.tools.jjq.value.JqValue;
import io.hyperfoil.tools.jjq.value.JqObject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
@Path("/api/data")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class DataResource {
@POST
public Response create(JqValue data) {
// data is parsed from the JSON request body by JqValueMessageBodyReader
// using JqValues.parse(byte[]) — zero intermediate String allocation
service.store(data);
return Response.ok().build();
}
@GET
@Path("{id}")
public JqValue get(@PathParam("id") long id) {
// JqValue is serialized to JSON by JqValueMessageBodyWriter
// using JqValue.toJsonString()
return service.load(id);
}
}The module includes a ParamConverterProvider that enables JqValue as a type for
@QueryParam, @PathParam, @HeaderParam, and @FormParam parameters:
@GET
@Path("/search")
public Response search(@QueryParam("filter") JqValue filter) {
// filter is parsed from the JSON query parameter string
// e.g., /search?filter={"status":"active","limit":10}
String status = filter.getField("status").asString("all");
return Response.ok(service.search(status)).build();
}The JqValueParamConverterProvider is annotated with @Provider and auto-discovered.
It parses JSON strings from request parameters via JqValues.parse() and serializes
back via JqValue.toJsonString().
Use the JqObject.builder() and JqArray.arrayBuilder() APIs to construct JSON values:
import io.hyperfoil.tools.jjq.value.*;
JqObject response = JqObject.builder()
.put("status", "ok")
.put("count", 42)
.put("items", JqArray.arrayBuilder()
.add(JqObject.builder().put("id", 1).put("name", "first").build())
.add(JqObject.builder().put("id", 2).put("name", "second").build())
.build())
.build();For records annotated with @JqMapped (from jjq-mapper), the module provides
JAX-RS providers that deserialize and serialize directly through jjq — bypassing
Jackson entirely:
import io.hyperfoil.tools.jjq.mapper.JqMapped;
import io.hyperfoil.tools.jjq.mapper.JqField;
@JqMapped
record CreateUserRequest(String name, int age, String email) {}
@JqMapped
record PerfResult(
String user,
@JqField(".config.timeout") int timeout,
@JqField(".data[0].name") String firstName
) {}Use these records directly as REST parameters and return types:
@POST
public Response createUser(CreateUserRequest request) {
// JSON bytes → JqValue → record via JqMapper (not Jackson)
service.create(request.name(), request.age(), request.email());
return Response.ok().build();
}
@GET
@Path("/{id}")
public CreateUserRequest getUser(@PathParam("id") long id) {
// Record → JqValue → JSON bytes via JqMapper (not Jackson)
return new CreateUserRequest("Alice", 30, "alice@example.com");
}The JqMappedMessageBodyReader and JqMappedMessageBodyWriter are annotated with
@Provider and auto-discovered. They only activate for types with @JqMapped — all
other types fall through to Jackson or other registered providers.
With the jjq-mapper-processor annotation processor on the classpath, mapping
executes in ~22 ns per 5-field record (5.8x faster than Jackson). Without the
processor, reflection-based mapping is used (~130 ns, comparable to Jackson).
For pure Jakarta EE environments that use JSON-B instead of Jackson (Open Liberty, Payara,
etc.), the module provides JsonbSerializer and JsonbDeserializer implementations:
import io.hyperfoil.tools.jjq.jakarta.JqValueJsonbSerializer;
import io.hyperfoil.tools.jjq.jakarta.JqValueJsonbDeserializer;
import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
JsonbConfig config = new JsonbConfig()
.withSerializers(new JqValueJsonbSerializer())
.withDeserializers(new JqValueJsonbDeserializer());
Jsonb jsonb = JsonbBuilder.create(config);
// Serialize
String json = jsonb.toJson(myJqValue, JqValue.class);
// Deserialize
JqValue value = jsonb.fromJson(jsonString, JqValue.class);You can also register the serializers on individual entity fields:
import jakarta.json.bind.annotation.JsonbTypeSerializer;
import jakarta.json.bind.annotation.JsonbTypeDeserializer;
public class MyEntity {
public String name;
@JsonbTypeSerializer(JqValueJsonbSerializer.class)
@JsonbTypeDeserializer(JqValueJsonbDeserializer.class)
public JqValue metadata;
}Once registered, JqValue fields in POJOs are automatically handled:
public class Config {
public String name;
public JqValue settings; // serialized as nested JSON
}
Config config = new Config();
config.name = "production";
config.settings = JqValues.parse("{\"timeout\":30,\"retries\":3}");
String json = jsonb.toJson(config);
// {"name":"production","settings":{"timeout":30,"retries":3}}
Config restored = jsonb.fromJson(json, Config.class);The module leverages jjq's optimized parser and serializer:
-
Parsing:
JqValueMessageBodyReaderusesJqValues.parse(byte[])for zero-intermediate-String parsing directly from the HTTP input stream. jjq's byte parser is 1.3-2.4x faster than Jackson on 10KB inputs with 26% less allocation. -
Serialization:
JqValueMessageBodyWriterusesJqValues.serializeTo(value, outputStream)which uses the direct byte serialization path — no intermediate String or StringBuilder. -
BYTEA persistence (JdbcType):
JqValueJdbcTypeusesserializeToByteOutput()withsetBinaryStream()for writes andparse(byte[])for reads. The write path achieves zero array copies — the pre-sized buffer is wrapped inByteArrayInputStreamand streamed directly to the JDBC driver. PostgreSQL'ssetBytes()makes a defensiveSystem.arraycopy;setBinaryStream(is, len)stores the stream reference and reads lazily during execute. For pass-through workloads (parse JSON → store → retrieve → serialize), deferred string values flow as raw bytes from input to database without ever constructing a Java String. -
JSONB persistence (FormatMapper):
JqValueFormatMappereliminates Jackson'sObjectMapperfrom the Hibernate serialization path. -
Immutability: Combined with
@Mutability(Immutability.class)on the entity field, both approaches avoiddeepCopyoverhead for second-level cache and dirty checking.