From 0d28b857ccf6fb4233a13f7133c66d6750c8ff0b Mon Sep 17 00:00:00 2001 From: Varun Madiath Date: Thu, 21 Aug 2025 15:15:49 -0400 Subject: [PATCH 1/4] Add a hasAdapter method to the JsonB interface Allows for checking if a type can be serialized/deserialized by the Jsonb instance without raising an Exception. --- jsonb/src/main/java/io/avaje/jsonb/Jsonb.java | 16 +++++ .../avaje/jsonb/core/CoreAdapterBuilder.java | 19 +++++ .../main/java/io/avaje/jsonb/core/DJsonb.java | 12 ++++ .../io/avaje/jsonb/core/HasAdapterTest.java | 71 +++++++++++++++++++ 4 files changed, 118 insertions(+) create mode 100644 jsonb/src/test/java/io/avaje/jsonb/core/HasAdapterTest.java diff --git a/jsonb/src/main/java/io/avaje/jsonb/Jsonb.java b/jsonb/src/main/java/io/avaje/jsonb/Jsonb.java index 7ece69a9..46838436 100644 --- a/jsonb/src/main/java/io/avaje/jsonb/Jsonb.java +++ b/jsonb/src/main/java/io/avaje/jsonb/Jsonb.java @@ -368,6 +368,22 @@ static Jsonb instance() { */ JsonAdapter rawAdapter(); + /** + * Check if a JsonAdapter exists for the given class without attempting to create one. + * + * @param cls The class to check for adapter availability + * @return true if an adapter exists or can be created, false otherwise + */ + boolean hasAdapter(Class cls); + + /** + * Check if a JsonAdapter exists for the given type without attempting to create one. + * + * @param type The type to check for adapter availability + * @return true if an adapter exists or can be created, false otherwise + */ + boolean hasAdapter(Type type); + /** * Build the Jsonb instance adding JsonAdapter, Factory or AdapterBuilder. */ diff --git a/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java b/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java index f25141ef..d4f34772 100644 --- a/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java +++ b/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java @@ -60,6 +60,25 @@ JsonAdapter get(Object cacheKey) { return (JsonAdapter) adapterCache.get(cacheKey); } + /** + * Check if an adapter exists or can be created for the given cache key. + * This performs a lightweight check without creating or caching adapters. + */ + boolean hasAdapter(Object cacheKey) { + // Fast path: check if already cached + if (adapterCache.containsKey(cacheKey)) { + return true; + } + + // Check if any factory can create this adapter + for (AdapterFactory factory : factories) { + if (factory.create((Type) cacheKey, context) == null) { + return true; + } + } + return false; + } + /** * Build for the simple non-annotated type case. */ diff --git a/jsonb/src/main/java/io/avaje/jsonb/core/DJsonb.java b/jsonb/src/main/java/io/avaje/jsonb/core/DJsonb.java index aacbd7a6..c9308298 100644 --- a/jsonb/src/main/java/io/avaje/jsonb/core/DJsonb.java +++ b/jsonb/src/main/java/io/avaje/jsonb/core/DJsonb.java @@ -212,6 +212,18 @@ public JsonAdapter rawAdapter() { return RawAdapter.STR; } + @Override + public boolean hasAdapter(Class cls) { + Type cacheKey = canonicalizeClass(requireNonNull(cls)); + return builder.hasAdapter(cacheKey); + } + + @Override + public boolean hasAdapter(Type type) { + type = removeSubtypeWildcard(canonicalize(requireNonNull(type))); + return builder.hasAdapter(type); + } + JsonReader objectReader(Object value) { return new ObjectJsonReader(value); } diff --git a/jsonb/src/test/java/io/avaje/jsonb/core/HasAdapterTest.java b/jsonb/src/test/java/io/avaje/jsonb/core/HasAdapterTest.java new file mode 100644 index 00000000..1b299cd5 --- /dev/null +++ b/jsonb/src/test/java/io/avaje/jsonb/core/HasAdapterTest.java @@ -0,0 +1,71 @@ +package io.avaje.jsonb.core; + +import io.avaje.jsonb.Jsonb; +import io.avaje.jsonb.Types; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Type; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class HasAdapterTest { + + private final Jsonb jsonb = Jsonb.builder().build(); + + @Test + @DisplayName("hasAdapter returns true for basic types and primitives") + void hasAdapter_basicTypes_returnsTrue() { + assertThat(jsonb.hasAdapter(String.class)).isTrue(); + assertThat(jsonb.hasAdapter(Integer.class)).isTrue(); + assertThat(jsonb.hasAdapter(Boolean.class)).isTrue(); + + assertThat(jsonb.hasAdapter(int.class)).isTrue(); + assertThat(jsonb.hasAdapter(boolean.class)).isTrue(); + assertThat(jsonb.hasAdapter(double.class)).isTrue(); + } + + @Test + @DisplayName("hasAdapter returns true for generic types like List, Map, and Optional") + void hasAdapter_withGenericTypes_returnsTrue() { + Type listOfString = Types.listOf(String.class); + assertThat(jsonb.hasAdapter(listOfString)).isTrue(); + + Type mapOfStringToInteger = Types.mapOf(Integer.class); + assertThat(jsonb.hasAdapter(mapOfStringToInteger)).isTrue(); + + Type optionalString = Types.newParameterizedType(Optional.class, String.class); + assertThat(jsonb.hasAdapter(optionalString)).isTrue(); + } + + @Test + @DisplayName("hasAdapter returns false for types without adapters") + void hasAdapter_whenAdapterNotExists_returnsFalse() { + assertThat(jsonb.hasAdapter(UnknownClass.class)).isFalse(); + assertThat(jsonb.hasAdapter(SomeInterface.class)).isFalse(); + } + + @Test + @DisplayName("hasAdapter works correctly with cached adapters") + void hasAdapter_withCachedAdapter_returnsTrue() { + jsonb.type(String.class); + assertThat(jsonb.hasAdapter(String.class)).isTrue(); + } + + @Test + @DisplayName("hasAdapter never throws exceptions, even for problematic types") + void hasAdapter_doesNotThrowExceptions() { + assertThat(jsonb.hasAdapter(UnknownClass.class)).isFalse(); + assertThat(jsonb.hasAdapter(SomeInterface.class)).isFalse(); + } + + // Test classes + private static class UnknownClass { + private String value; + } + + private interface SomeInterface { + String getValue(); + } +} From 30b1ed46ee9172c3f6f24384652196672cd4a064 Mon Sep 17 00:00:00 2001 From: Varun Madiath Date: Thu, 21 Aug 2025 15:40:43 -0400 Subject: [PATCH 2/4] Have the hasAdapter cache the adapter if it exists --- .../avaje/jsonb/core/CoreAdapterBuilder.java | 63 ++++++++++++------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java b/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java index d4f34772..72649da0 100644 --- a/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java +++ b/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java @@ -62,7 +62,7 @@ JsonAdapter get(Object cacheKey) { /** * Check if an adapter exists or can be created for the given cache key. - * This performs a lightweight check without creating or caching adapters. + * If an adapter can be created, it will be cached for subsequent use. */ boolean hasAdapter(Object cacheKey) { // Fast path: check if already cached @@ -70,27 +70,21 @@ boolean hasAdapter(Object cacheKey) { return true; } - // Check if any factory can create this adapter - for (AdapterFactory factory : factories) { - if (factory.create((Type) cacheKey, context) == null) { - return true; - } - } - return false; + // Try to create and cache adapter + JsonAdapter adapter = tryCreateAdapter((Type) cacheKey, cacheKey, false); + return adapter != null; } /** - * Build for the simple non-annotated type case. - */ - JsonAdapter build(Type type) { - return build(type, type); - } - - /** - * Build given type and annotations. + * Try to create an adapter for the given type, with optional exception handling. + * + * @param type The type to create an adapter for + * @param cacheKey The cache key to use + * @return The created adapter, or null if no factory can create it and throwOnFailure is false + * @throws IllegalArgumentException if no adapter found and throwOnFailure is true */ @SuppressWarnings("unchecked") - JsonAdapter build(Type type, Object cacheKey) { + private JsonAdapter tryCreateAdapter(Type type, Object cacheKey, boolean throwOnFailure) { LookupChain lookupChain = lookupChainThreadLocal.get(); if (lookupChain == null) { lookupChain = new LookupChain(); @@ -113,19 +107,40 @@ JsonAdapter build(Type type, Object cacheKey) { return result; } } - throw new IllegalArgumentException( - "No JsonAdapter for " - + type - + "\nPossible Causes: \n" - + "1. Missing @Json or @Json.Import annotation.\n" - + "2. The avaje-jsonb-generator dependency was not available during compilation\n"); + + if (throwOnFailure) { + throw new IllegalArgumentException( + "No JsonAdapter for " + + type + + "\nPossible Causes: \n" + + "1. Missing @Json or @Json.Import annotation.\n" + + "2. The avaje-jsonb-generator dependency was not available during compilation\n"); + } + return null; // No adapter found } catch (IllegalArgumentException e) { - throw lookupChain.exceptionWithLookupStack(e); + if (throwOnFailure) { + throw lookupChain.exceptionWithLookupStack(e); + } + return null; } finally { lookupChain.pop(success); } } + /** + * Build for the simple non-annotated type case. + */ + JsonAdapter build(Type type) { + return build(type, type); + } + + /** + * Build given type and annotations. + */ + JsonAdapter build(Type type, Object cacheKey) { + return tryCreateAdapter(type, cacheKey, true); + } + /** * A possibly-reentrant chain of lookups for JSON adapters. * From c583491649edc1dce1ae3243034de29962f6943a Mon Sep 17 00:00:00 2001 From: Josiah Noel <32279667+SentryMan@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:07:06 -0400 Subject: [PATCH 3/4] cleanup --- jsonb/src/main/java/io/avaje/jsonb/Jsonb.java | 26 ++++++++++++------- .../avaje/jsonb/core/CoreAdapterBuilder.java | 10 +++---- .../main/java/io/avaje/jsonb/core/DJsonb.java | 4 +-- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/jsonb/src/main/java/io/avaje/jsonb/Jsonb.java b/jsonb/src/main/java/io/avaje/jsonb/Jsonb.java index 46838436..da07a734 100644 --- a/jsonb/src/main/java/io/avaje/jsonb/Jsonb.java +++ b/jsonb/src/main/java/io/avaje/jsonb/Jsonb.java @@ -235,6 +235,8 @@ static Jsonb instance() { * When using Object.class and reading fromJson() then the java types used in * the result are determined dynamically based on the json types being read and the resulting java types * are ArrayList, LinkedHashMap, String, boolean, and double. + * + * @throws IllegalStateException if an adapter cannot be found */ JsonType type(Class cls); @@ -279,6 +281,8 @@ static Jsonb instance() { * When using Object.class and reading fromJson() then the java types used in * the result are determined dynamically based on the json types being read and the resulting java types * are ArrayList, LinkedHashMap, String, boolean, and double. + * + * @throws IllegalStateException if an adapter cannot be found */ JsonType type(Type type); @@ -341,6 +345,8 @@ static Jsonb instance() { * *

JsonAdapter is generally used by generated serialization code. Application code should use * {@link Jsonb#type(Class)} and {@link JsonType} instead. + * + * @throws IllegalStateException if an adapter cannot be found */ JsonAdapter adapter(Class cls); @@ -349,6 +355,8 @@ static Jsonb instance() { * *

JsonAdapter is generally used by generated serialization code. Application code should use * {@link Jsonb#type(Type)} and {@link JsonType} instead. + * + * @throws IllegalStateException if an adapter cannot be found */ JsonAdapter adapter(Type type); @@ -369,20 +377,20 @@ static Jsonb instance() { JsonAdapter rawAdapter(); /** - * Check if a JsonAdapter exists for the given class without attempting to create one. - * + * Check if a JsonAdapter exists for the given class. + * * @param cls The class to check for adapter availability - * @return true if an adapter exists or can be created, false otherwise + * @return true if an adapter exists, false otherwise */ - boolean hasAdapter(Class cls); + boolean hasAdapter(Class cls); /** - * Check if a JsonAdapter exists for the given type without attempting to create one. - * - * @param type The type to check for adapter availability - * @return true if an adapter exists or can be created, false otherwise + * Check if a JsonAdapter exists for the given type. + * + * @param type The type to check for adapter availability + * @return true if an adapter exists, false otherwise */ - boolean hasAdapter(Type type); + boolean hasAdapter(Type type); /** * Build the Jsonb instance adding JsonAdapter, Factory or AdapterBuilder. diff --git a/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java b/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java index 72649da0..2d896dce 100644 --- a/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java +++ b/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java @@ -64,14 +64,12 @@ JsonAdapter get(Object cacheKey) { * Check if an adapter exists or can be created for the given cache key. * If an adapter can be created, it will be cached for subsequent use. */ - boolean hasAdapter(Object cacheKey) { - // Fast path: check if already cached + boolean hasAdapter(Type cacheKey) { if (adapterCache.containsKey(cacheKey)) { return true; } - // Try to create and cache adapter - JsonAdapter adapter = tryCreateAdapter((Type) cacheKey, cacheKey, false); + JsonAdapter adapter = lookupAdapter(cacheKey, cacheKey, false); return adapter != null; } @@ -84,7 +82,7 @@ boolean hasAdapter(Object cacheKey) { * @throws IllegalArgumentException if no adapter found and throwOnFailure is true */ @SuppressWarnings("unchecked") - private JsonAdapter tryCreateAdapter(Type type, Object cacheKey, boolean throwOnFailure) { + private JsonAdapter lookupAdapter(Type type, Object cacheKey, boolean throwOnFailure) { LookupChain lookupChain = lookupChainThreadLocal.get(); if (lookupChain == null) { lookupChain = new LookupChain(); @@ -138,7 +136,7 @@ JsonAdapter build(Type type) { * Build given type and annotations. */ JsonAdapter build(Type type, Object cacheKey) { - return tryCreateAdapter(type, cacheKey, true); + return lookupAdapter(type, cacheKey, true); } /** diff --git a/jsonb/src/main/java/io/avaje/jsonb/core/DJsonb.java b/jsonb/src/main/java/io/avaje/jsonb/core/DJsonb.java index c9308298..7543969a 100644 --- a/jsonb/src/main/java/io/avaje/jsonb/core/DJsonb.java +++ b/jsonb/src/main/java/io/avaje/jsonb/core/DJsonb.java @@ -213,13 +213,13 @@ public JsonAdapter rawAdapter() { } @Override - public boolean hasAdapter(Class cls) { + public boolean hasAdapter(Class cls) { Type cacheKey = canonicalizeClass(requireNonNull(cls)); return builder.hasAdapter(cacheKey); } @Override - public boolean hasAdapter(Type type) { + public boolean hasAdapter(Type type) { type = removeSubtypeWildcard(canonicalize(requireNonNull(type))); return builder.hasAdapter(type); } From c3ae4b7e4b5188bc48da023cadc26781bfd770c6 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Fri, 22 Aug 2025 11:36:40 +1200 Subject: [PATCH 4/4] Format only changes --- .../io/avaje/jsonb/core/CoreAdapterBuilder.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java b/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java index 2d896dce..4b1d8de2 100644 --- a/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java +++ b/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java @@ -68,9 +68,7 @@ boolean hasAdapter(Type cacheKey) { if (adapterCache.containsKey(cacheKey)) { return true; } - - JsonAdapter adapter = lookupAdapter(cacheKey, cacheKey, false); - return adapter != null; + return lookupAdapter(cacheKey, cacheKey, false) != null; } /** @@ -108,11 +106,11 @@ private JsonAdapter lookupAdapter(Type type, Object cacheKey, boolean thr if (throwOnFailure) { throw new IllegalArgumentException( - "No JsonAdapter for " - + type - + "\nPossible Causes: \n" - + "1. Missing @Json or @Json.Import annotation.\n" - + "2. The avaje-jsonb-generator dependency was not available during compilation\n"); + "No JsonAdapter for " + + type + + "\nPossible Causes: \n" + + "1. Missing @Json or @Json.Import annotation.\n" + + "2. The avaje-jsonb-generator dependency was not available during compilation\n"); } return null; // No adapter found } catch (IllegalArgumentException e) {