Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.core.TSFBuilder;
import com.fasterxml.jackson.core.util.BufferRecycler;
import com.fasterxml.jackson.core.util.JsonRecyclerPools;
import com.fasterxml.jackson.core.util.RecyclerPool;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand All @@ -40,6 +43,24 @@ public final class ObjectMappers {

private ObjectMappers() {}

/**
* Selects how Jackson recycles the {@link BufferRecycler} instances that hold the reusable
* {@code byte[]}/{@code char[]} buffers used while parsing and generating.
*/
public enum RecyclerPoolType {
/**
* Jackson's default: one recycler per thread. Ideal for a platform thread-pool execution model, where a
* small number of long-lived workers each reuse their recycler (and its buffers) across many requests.
*/
THREAD_LOCAL,
/**
* A thread-identity-independent shared pool. Prefer this under a thread-per-task virtual-thread execution
* model, where {@link #THREAD_LOCAL} would reallocate buffers for every short-lived virtual thread instead
* of recycling them.
*/
SHARED;
}

/**
* Returns a default ObjectMapper with settings adjusted for use in clients.
*
Expand All @@ -50,7 +71,11 @@ private ObjectMappers() {}
* </ul>
*/
public static JsonMapper newClientJsonMapper() {
return withDefaultModules(JsonMapper.builder(jsonFactory()))
return newClientJsonMapper(RecyclerPoolType.THREAD_LOCAL);
}

public static JsonMapper newClientJsonMapper(RecyclerPoolType recyclerPool) {
return withDefaultModules(JsonMapper.builder(jsonFactory(recyclerPool)))
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
}
Expand All @@ -65,7 +90,11 @@ public static JsonMapper newClientJsonMapper() {
* </ul>
*/
public static CBORMapper newClientCborMapper() {
return withDefaultModules(CBORMapper.builder(cborFactory()))
return newClientCborMapper(RecyclerPoolType.THREAD_LOCAL);
}

public static CBORMapper newClientCborMapper(RecyclerPoolType recyclerPool) {
return withDefaultModules(CBORMapper.builder(cborFactory(recyclerPool)))
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
}
Expand All @@ -81,7 +110,11 @@ public static CBORMapper newClientCborMapper() {
* </ul>
*/
public static SmileMapper newClientSmileMapper() {
return withDefaultModules(SmileMapper.builder(smileFactory()))
return newClientSmileMapper(RecyclerPoolType.THREAD_LOCAL);
}

public static SmileMapper newClientSmileMapper(RecyclerPoolType recyclerPool) {
return withDefaultModules(SmileMapper.builder(smileFactory(recyclerPool)))
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
}
Expand All @@ -96,7 +129,11 @@ public static SmileMapper newClientSmileMapper() {
* </ul>
*/
public static JsonMapper newServerJsonMapper() {
return withDefaultModules(JsonMapper.builder(jsonFactory()))
return newServerJsonMapper(RecyclerPoolType.THREAD_LOCAL);
}

public static JsonMapper newServerJsonMapper(RecyclerPoolType recyclerPool) {
return withDefaultModules(JsonMapper.builder(jsonFactory(recyclerPool)))
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
}
Expand All @@ -111,7 +148,11 @@ public static JsonMapper newServerJsonMapper() {
* </ul>
*/
public static CBORMapper newServerCborMapper() {
return withDefaultModules(CBORMapper.builder(cborFactory()))
return newServerCborMapper(RecyclerPoolType.THREAD_LOCAL);
}

public static CBORMapper newServerCborMapper(RecyclerPoolType recyclerPool) {
return withDefaultModules(CBORMapper.builder(cborFactory(recyclerPool)))
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
}
Expand All @@ -127,7 +168,11 @@ public static CBORMapper newServerCborMapper() {
* </ul>
*/
public static SmileMapper newServerSmileMapper() {
return withDefaultModules(SmileMapper.builder(smileFactory()))
return newServerSmileMapper(RecyclerPoolType.THREAD_LOCAL);
}

public static SmileMapper newServerSmileMapper(RecyclerPoolType recyclerPool) {
return withDefaultModules(SmileMapper.builder(smileFactory(recyclerPool)))
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
}
Expand All @@ -136,26 +181,50 @@ public static ObjectMapper newClientObjectMapper() {
return newClientJsonMapper();
}

public static ObjectMapper newClientObjectMapper(RecyclerPoolType recyclerPool) {
return newClientJsonMapper(recyclerPool);
}

public static ObjectMapper newCborClientObjectMapper() {
return newClientCborMapper();
}

public static ObjectMapper newCborClientObjectMapper(RecyclerPoolType recyclerPool) {
return newClientCborMapper(recyclerPool);
}

public static ObjectMapper newSmileClientObjectMapper() {
return newClientSmileMapper();
}

public static ObjectMapper newSmileClientObjectMapper(RecyclerPoolType recyclerPool) {
return newClientSmileMapper(recyclerPool);
}

public static ObjectMapper newServerObjectMapper() {
return newServerJsonMapper();
}

public static ObjectMapper newServerObjectMapper(RecyclerPoolType recyclerPool) {
return newServerJsonMapper(recyclerPool);
}

public static ObjectMapper newCborServerObjectMapper() {
return newServerCborMapper();
}

public static ObjectMapper newCborServerObjectMapper(RecyclerPoolType recyclerPool) {
return newServerCborMapper(recyclerPool);
}

public static ObjectMapper newSmileServerObjectMapper() {
return newServerSmileMapper();
}

public static ObjectMapper newSmileServerObjectMapper(RecyclerPoolType recyclerPool) {
return newServerSmileMapper(recyclerPool);
}

/**
* Configures provided MapperBuilder with default modules and settings.
*
Expand Down Expand Up @@ -230,23 +299,41 @@ public static ObjectMapper withDefaultModules(ObjectMapper mapper) {

/** Creates a new {@link JsonFactory} configured with Conjure defaults. */
public static JsonFactory jsonFactory() {
return withDefaults(InstrumentedJsonFactory.builder()).build();
return jsonFactory(RecyclerPoolType.THREAD_LOCAL);
}

/** Creates a new {@link JsonFactory} configured with Conjure defaults and the given recycler pool. */
public static JsonFactory jsonFactory(RecyclerPoolType recyclerPool) {
return withDefaults(InstrumentedJsonFactory.builder(), recyclerPool).build();
}

/** Creates a new {@link SmileFactory} configured with Conjure defaults. */
public static SmileFactory smileFactory() {
return withDefaults(InstrumentedSmileFactory.builder().disable(SmileGenerator.Feature.ENCODE_BINARY_AS_7BIT))
return smileFactory(RecyclerPoolType.THREAD_LOCAL);
}

/** Creates a new {@link SmileFactory} configured with Conjure defaults and the given recycler pool. */
public static SmileFactory smileFactory(RecyclerPoolType recyclerPool) {
return withDefaults(
InstrumentedSmileFactory.builder().disable(SmileGenerator.Feature.ENCODE_BINARY_AS_7BIT),
recyclerPool)
.build();
}

/** Creates a new {@link CBORFactory} configured with Conjure defaults. */
public static CBORFactory cborFactory() {
return withDefaults(CBORFactory.builder()).build();
return cborFactory(RecyclerPoolType.THREAD_LOCAL);
}

/** Configures provided JsonFactory with Conjure default settings. */
private static <F extends JsonFactory, B extends TSFBuilder<F, B>> B withDefaults(B builder) {
return builder
/** Creates a new {@link CBORFactory} configured with Conjure defaults and the given recycler pool. */
public static CBORFactory cborFactory(RecyclerPoolType recyclerPool) {
return withDefaults(CBORFactory.builder(), recyclerPool).build();
}

/** Configures provided JsonFactory with Conjure default settings and the given recycler pool. */
private static <F extends JsonFactory, B extends TSFBuilder<F, B>> B withDefaults(
B builder, RecyclerPoolType recyclerPool) {
return builder.recyclerPool(jacksonRecyclerPool(recyclerPool))
// Interning introduces excessive contention https://github.com/FasterXML/jackson-core/issues/946
.disable(JsonFactory.Feature.INTERN_FIELD_NAMES)
// Canonicalization can be helpful to avoid string re-allocation, however we expect unbounded
Expand All @@ -260,4 +347,11 @@ private static <F extends JsonFactory, B extends TSFBuilder<F, B>> B withDefault
.maxStringLength(50_000_000)
.build());
}

private static RecyclerPool<BufferRecycler> jacksonRecyclerPool(RecyclerPoolType recyclerPool) {
return switch (recyclerPool) {
case THREAD_LOCAL -> JsonRecyclerPools.threadLocalPool();
case SHARED -> JsonRecyclerPools.sharedConcurrentDequePool();
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* (c) Copyright 2026 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.conjure.java.serialization;

import static org.assertj.core.api.Assertions.assertThat;

import com.fasterxml.jackson.core.util.JsonRecyclerPools;
import com.palantir.conjure.java.serialization.ObjectMappers.RecyclerPoolType;
import org.junit.jupiter.api.Test;

final class ObjectMappersRecyclerPoolTest {

@Test
void defaults_to_the_thread_local_recycler_pool() {
assertThat(ObjectMappers.newServerJsonMapper().getFactory()._getRecyclerPool())
.isSameAs(JsonRecyclerPools.threadLocalPool());
assertThat(ObjectMappers.newClientJsonMapper().getFactory()._getRecyclerPool())
.isSameAs(JsonRecyclerPools.threadLocalPool());
}

@Test
void thread_local_recycler_pool_when_requested() {
assertThat(ObjectMappers.newServerJsonMapper(RecyclerPoolType.THREAD_LOCAL)
.getFactory()
._getRecyclerPool())
.isSameAs(JsonRecyclerPools.threadLocalPool());
}

@Test
void shared_recycler_pool_when_requested() {
// Covers JSON, Smile, and CBOR, for both server and client mappers.
assertThat(ObjectMappers.newServerJsonMapper(RecyclerPoolType.SHARED)
.getFactory()
._getRecyclerPool())
.isSameAs(JsonRecyclerPools.sharedConcurrentDequePool());
assertThat(ObjectMappers.newServerSmileMapper(RecyclerPoolType.SHARED)
.getFactory()
._getRecyclerPool())
.isSameAs(JsonRecyclerPools.sharedConcurrentDequePool());
assertThat(ObjectMappers.newServerCborMapper(RecyclerPoolType.SHARED)
.getFactory()
._getRecyclerPool())
.isSameAs(JsonRecyclerPools.sharedConcurrentDequePool());
assertThat(ObjectMappers.newClientJsonMapper(RecyclerPoolType.SHARED)
.getFactory()
._getRecyclerPool())
.isSameAs(JsonRecyclerPools.sharedConcurrentDequePool());
}

@Test
void object_mapper_aliases_propagate_the_recycler_pool() {
assertThat(ObjectMappers.newServerObjectMapper(RecyclerPoolType.SHARED)
.getFactory()
._getRecyclerPool())
.isSameAs(JsonRecyclerPools.sharedConcurrentDequePool());
assertThat(ObjectMappers.newServerObjectMapper().getFactory()._getRecyclerPool())
.isSameAs(JsonRecyclerPools.threadLocalPool());
}
}