diff --git a/jsonb/src/main/java/io/avaje/jsonb/Jsonb.java b/jsonb/src/main/java/io/avaje/jsonb/Jsonb.java index 7ece69a9..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); @@ -368,6 +376,22 @@ static Jsonb instance() { */ JsonAdapter rawAdapter(); + /** + * Check if a JsonAdapter exists for the given class. + * + * @param cls The class to check for adapter availability + * @return true if an adapter exists, false otherwise + */ + boolean hasAdapter(Class cls); + + /** + * 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); + /** * 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..4b1d8de2 100644 --- a/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java +++ b/jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java @@ -61,17 +61,26 @@ JsonAdapter get(Object cacheKey) { } /** - * Build for the simple non-annotated type case. + * 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. */ - JsonAdapter build(Type type) { - return build(type, type); + boolean hasAdapter(Type cacheKey) { + if (adapterCache.containsKey(cacheKey)) { + return true; + } + return lookupAdapter(cacheKey, cacheKey, false) != null; } /** - * 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 lookupAdapter(Type type, Object cacheKey, boolean throwOnFailure) { LookupChain lookupChain = lookupChainThreadLocal.get(); if (lookupChain == null) { lookupChain = new LookupChain(); @@ -94,19 +103,40 @@ JsonAdapter build(Type type, Object cacheKey) { return result; } } - throw new IllegalArgumentException( + + 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"); + + 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 lookupAdapter(type, cacheKey, true); + } + /** * A possibly-reentrant chain of lookups for JSON adapters. * 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..7543969a 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(); + } +}