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();
+ }
+}