This standalone example shows how to consume a JSONEachRow response with the
client-v2 JSONEachRowFormatReader and a JsonParserFactory. The two
factories shipped with the client are the customization points:
JacksonJsonParserFactoryexposes aprotected ObjectMapper createMapper()hook — override it to return a fully configuredObjectMapper(modules, feature flags, custom deserializers, etc.).GsonJsonParserFactoryexposes aprotected void customize(GsonBuilder)hook — override it to configure theGsonBuilder(number policy, type adapters, etc.). The factory still appliessetLenient()on its own afterwards, which is required for the stream-of-objects shape ofJSONEachRow.
The example is structured as a small component:
ClientV2JsonProcessorsExample(Client client)holds the sharedClientand exposes regular instance methods (recreateTable(),loadSampleData(),readAll(label, factory),run()), so the class can be copied as-is into another project and have its individual methods invoked.- Sample rows are kept in a plain
Object[][]constant, separate from the SQL, so the read path stays focused on the parser factory. - Two small subclasses,
CustomJacksonParserFactoryandCustomGsonParserFactory, demonstrate the protected-hook customization. Both also implement a tinyPayloadConverterinterface defined inside the example: their configuredObjectMapper/Gsonis reused to convert the rawpayloadMapproduced by the underlying library into a typedPayloadPOJO. The default factories do not implement the interface, soreadAll(...)logs the raw map for them — making the contrast between the default behavior and the customized behavior visible in the output.
Each read call in run() follows the same three-step shape:
- Create the factory —
new JacksonJsonParserFactory()/new GsonJsonParserFactory()for defaults, or an instance of a custom subclass. - Customize if needed — only inside the subclass, by overriding the protected hook.
- Execute —
readAll(label, factory)runs theSELECTand feeds the response stream throughnew JSONEachRowFormatReader(factory.createJsonParser(...)).
The client example selects the output format with
new QuerySettings().setFormat(ClickHouseFormat.JSONEachRow). Use that form
instead of appending FORMAT JSONEachRow to the SQL when calling client-v2
directly when you enable client-side JSON number output settings, because
those settings depend on the request format.
ClickHouse 64-bit integers can be larger than the exact integer range of a
JSON floating-point number. Jackson's default map materialization preserves
ordinary integer tokens as integer Number values. Gson's default
Map<String, Object> materialization may surface numbers as floating-point
values, which can round large integers before getLong(...) sees them.
For Gson, extend GsonJsonParserFactory and configure the object number
strategy:
public final class PreciseGsonJsonParserFactory extends GsonJsonParserFactory {
@Override
protected void customize(GsonBuilder builder) {
builder.setObjectToNumberStrategy(com.google.gson.ToNumberPolicy.LONG_OR_DOUBLE);
}
}The included CustomGsonParserFactory uses this pattern. Use
ToNumberPolicy.BIG_DECIMAL instead when exact decimal representation matters
more than receiving integer tokens as Long.
- JDK 17 or newer
- A running ClickHouse server reachable from the machine running the example
- A locally installed
client-v2snapshot from this repository
From this directory:
mvn compile exec:javaConnection properties can be supplied as system properties:
-DchEndpoint— endpoint to connect to (default:http://localhost:8123)-DchUser— ClickHouse user name (default:default)-DchPassword— ClickHouse user password (default: empty)-DchDatabase— ClickHouse database name (default:default)
Example with custom connection properties:
mvn compile exec:java \
-DchEndpoint=http://localhost:8123 \
-DchUser=default \
-DchPassword=secret \
-DchDatabase=defaultcom.clickhouse.examples.client_v2.json_processors.ClientV2JsonProcessorsExample
Steps performed by run():
recreateTable()— drops and re-createsclient_v2_json_processors_examplewith primitive columns and onepayload JSONcolumn.loadSampleData()— inserts the rows from theSAMPLE_ROWSarray as a single batchedINSERT.readAll(...)is invoked four times, each time with a differentJsonParserFactory:- default
JacksonJsonParserFactory; CustomJacksonParserFactory, which overridescreateMapper()to tolerate unknown properties and preserve big integers and decimals exactly, and implementsPayloadConverterto convert each row'spayloadMapinto aPayloadPOJO viaObjectMapper.convertValue(...);- default
GsonJsonParserFactory; CustomGsonParserFactory, which overridescustomize(GsonBuilder)to use aLONG_OR_DOUBLEnumber policy and disable HTML escaping, and implementsPayloadConverterto convert each row'spayloadMapinto aPayloadPOJO viagson.fromJson(gson.toJsonTree(...)).
- default
Logged rows include the payload value's runtime class so the difference
between the default factories (which surface a LinkedHashMap /
LinkedTreeMap) and the customized factories (which surface a Payload)
shows up directly in the output.
The build keeps both jackson-databind and gson on the classpath so the
example can switch between processors at runtime. Production applications
only need to keep the processor they actually use.