Created
April 5, 2026 12:42
-
-
Save vorburger/8f9e9f3a524ca13898a46fa0d0b54357 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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