Skip to content

Instantly share code, notes, and snippets.

@vorburger
Created April 5, 2026 12:42
Show Gist options
  • Select an option

  • Save vorburger/8f9e9f3a524ca13898a46fa0d0b54357 to your computer and use it in GitHub Desktop.

Select an option

Save vorburger/8f9e9f3a524ca13898a46fa0d0b54357 to your computer and use it in GitHub Desktop.
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2026 The Enola <https://enola.dev> Authors
*
* 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
*
* https://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 dev.enola.common.http.server;
import io.javalin.json.JsonMapper;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
/**
* JavalinJackson23 delegates to another JsonMapper, depending on the presence of Jackson 2
* annotations on the target type.
*
* <p><a href="https://github.com/javalin/javalin/issues/2571">See this
* discussion for background</a>.
*
* @author <a href="https://www.vorburger.ch">Michael Vorburger.ch</a>
*/
public class JavalinJackson23 implements JsonMapper {
// TODO: Remove from Enola.dev after https://github.com/googleapis/java-genai/issues/868
// Cache to avoid repeated class hierarchy walks on hot paths.
private static final ConcurrentHashMap<Class<?>, Boolean> JACKSON2_CACHE =
new ConcurrentHashMap<>();
private final JsonMapper jackson2mapper;
private final JsonMapper jackson3mapper;
public JavalinJackson23(JsonMapper jackson2mapper, JsonMapper jackson3mapper) {
this.jackson2mapper = jackson2mapper;
this.jackson3mapper = jackson3mapper;
}
@Override
@SuppressWarnings({"TypeParameterUnusedInFormals"})
public <T> T fromJsonString(String json, Type targetType) {
if (targetType instanceof Class<?> clazz && requiresJackson2(clazz))
return jackson2mapper.fromJsonString(json, targetType);
else return jackson3mapper.fromJsonString(json, targetType);
}
@Override
@SuppressWarnings({"TypeParameterUnusedInFormals"})
public <T> T fromJsonStream(InputStream json, Type targetType) {
if (targetType instanceof Class<?> clazz && requiresJackson2(clazz))
return jackson2mapper.fromJsonStream(json, targetType);
else return jackson3mapper.fromJsonStream(json, targetType);
}
@Override
public String toJsonString(Object obj, Type type) {
if (type instanceof Class<?> clazz && requiresJackson2(clazz))
return jackson2mapper.toJsonString(obj, type);
else return jackson3mapper.toJsonString(obj, type);
}
@Override
public InputStream toJsonStream(Object obj, Type type) {
if (type instanceof Class<?> clazz && requiresJackson2(clazz))
return jackson2mapper.toJsonStream(obj, type);
else return jackson3mapper.toJsonStream(obj, type);
}
@Override
public void writeToOutputStream(Stream<?> stream, OutputStream outputStream) {
try {
outputStream.write('[');
var iterator = stream.iterator();
while (iterator.hasNext()) {
var obj = iterator.next();
if (obj == null) {
outputStream.write("null".getBytes(StandardCharsets.UTF_8));
} else {
Class<?> clazz = obj.getClass();
if (requiresJackson2(clazz)) {
outputStream.write(
jackson2mapper
.toJsonString(obj, clazz)
.getBytes(StandardCharsets.UTF_8));
} else {
outputStream.write(
jackson3mapper
.toJsonString(obj, clazz)
.getBytes(StandardCharsets.UTF_8));
}
}
if (iterator.hasNext()) {
outputStream.write(',');
}
}
outputStream.write(']');
} catch (Exception e) {
throw new RuntimeException("Failed to serialize stream to JSON", e);
}
}
/**
* Returns true if {@code clazz} (or any superclass) carries Jackson 2 ({@code
* com.fasterxml.jackson}) annotations. Jackson 2 must handle such types; Jackson 3 does not
* process {@code com.fasterxml.jackson.databind.annotation} annotations from Jackson 2.
*
* <p>The superclass walk is required because AutoValue generates a concrete subclass (e.g.
* {@code AutoValue_GenerateContentResponse}) whose parent abstract class carries the
* annotations (e.g. {@link com.google.genai.types.GenerateContentResponse}).
*/
private static boolean requiresJackson2(Class<?> clazz) {
return JACKSON2_CACHE.computeIfAbsent(clazz, JavalinJackson23::hasJackson2Annotation);
}
private static boolean hasJackson2Annotation(Class<?> clazz) {
for (Class<?> c = clazz; c != null && c != Object.class; c = c.getSuperclass()) {
for (var annotation : c.getDeclaredAnnotations()) {
if (annotation.annotationType().getName().startsWith("com.fasterxml.jackson")) {
return true;
}
}
}
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment