Skip to content

Commit 044d49f

Browse files
authored
Add a hasAdapter method to the JsonB interface (#415)
* 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. * Have the hasAdapter cache the adapter if it exists
1 parent c73a308 commit 044d49f

File tree

4 files changed

+148
-11
lines changed

4 files changed

+148
-11
lines changed

jsonb/src/main/java/io/avaje/jsonb/Jsonb.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ static Jsonb instance() {
235235
* When using <code>Object.class</code> and reading <code>fromJson()</code> then the java types used in
236236
* the result are determined dynamically based on the json types being read and the resulting java types
237237
* are ArrayList, LinkedHashMap, String, boolean, and double.
238+
*
239+
* @throws IllegalStateException if an adapter cannot be found
238240
*/
239241
<T> JsonType<T> type(Class<T> cls);
240242

@@ -279,6 +281,8 @@ static Jsonb instance() {
279281
* When using <code>Object.class</code> and reading <code>fromJson()</code> then the java types used in
280282
* the result are determined dynamically based on the json types being read and the resulting java types
281283
* are ArrayList, LinkedHashMap, String, boolean, and double.
284+
*
285+
* @throws IllegalStateException if an adapter cannot be found
282286
*/
283287
<T> JsonType<T> type(Type type);
284288

@@ -341,6 +345,8 @@ static Jsonb instance() {
341345
*
342346
* <p>JsonAdapter is generally used by generated serialization code. Application code should use
343347
* {@link Jsonb#type(Class)} and {@link JsonType} instead.
348+
*
349+
* @throws IllegalStateException if an adapter cannot be found
344350
*/
345351
<T> JsonAdapter<T> adapter(Class<T> cls);
346352

@@ -349,6 +355,8 @@ static Jsonb instance() {
349355
*
350356
* <p>JsonAdapter is generally used by generated serialization code. Application code should use
351357
* {@link Jsonb#type(Type)} and {@link JsonType} instead.
358+
*
359+
* @throws IllegalStateException if an adapter cannot be found
352360
*/
353361
<T> JsonAdapter<T> adapter(Type type);
354362

@@ -368,6 +376,22 @@ static Jsonb instance() {
368376
*/
369377
JsonAdapter<String> rawAdapter();
370378

379+
/**
380+
* Check if a JsonAdapter exists for the given class.
381+
*
382+
* @param cls The class to check for adapter availability
383+
* @return true if an adapter exists, false otherwise
384+
*/
385+
boolean hasAdapter(Class<?> cls);
386+
387+
/**
388+
* Check if a JsonAdapter exists for the given type.
389+
*
390+
* @param type The type to check for adapter availability
391+
* @return true if an adapter exists, false otherwise
392+
*/
393+
boolean hasAdapter(Type type);
394+
371395
/**
372396
* Build the Jsonb instance adding JsonAdapter, Factory or AdapterBuilder.
373397
*/

jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,26 @@ <T> JsonAdapter<T> get(Object cacheKey) {
6161
}
6262

6363
/**
64-
* Build for the simple non-annotated type case.
64+
* Check if an adapter exists or can be created for the given cache key.
65+
* If an adapter can be created, it will be cached for subsequent use.
6566
*/
66-
<T> JsonAdapter<T> build(Type type) {
67-
return build(type, type);
67+
boolean hasAdapter(Type cacheKey) {
68+
if (adapterCache.containsKey(cacheKey)) {
69+
return true;
70+
}
71+
return lookupAdapter(cacheKey, cacheKey, false) != null;
6872
}
6973

7074
/**
71-
* Build given type and annotations.
75+
* Try to create an adapter for the given type, with optional exception handling.
76+
*
77+
* @param type The type to create an adapter for
78+
* @param cacheKey The cache key to use
79+
* @return The created adapter, or null if no factory can create it and throwOnFailure is false
80+
* @throws IllegalArgumentException if no adapter found and throwOnFailure is true
7281
*/
7382
@SuppressWarnings("unchecked")
74-
<T> JsonAdapter<T> build(Type type, Object cacheKey) {
83+
private <T> JsonAdapter<T> lookupAdapter(Type type, Object cacheKey, boolean throwOnFailure) {
7584
LookupChain lookupChain = lookupChainThreadLocal.get();
7685
if (lookupChain == null) {
7786
lookupChain = new LookupChain();
@@ -94,19 +103,40 @@ <T> JsonAdapter<T> build(Type type, Object cacheKey) {
94103
return result;
95104
}
96105
}
97-
throw new IllegalArgumentException(
106+
107+
if (throwOnFailure) {
108+
throw new IllegalArgumentException(
98109
"No JsonAdapter for "
99-
+ type
100-
+ "\nPossible Causes: \n"
101-
+ "1. Missing @Json or @Json.Import annotation.\n"
102-
+ "2. The avaje-jsonb-generator dependency was not available during compilation\n");
110+
+ type
111+
+ "\nPossible Causes: \n"
112+
+ "1. Missing @Json or @Json.Import annotation.\n"
113+
+ "2. The avaje-jsonb-generator dependency was not available during compilation\n");
114+
}
115+
return null; // No adapter found
103116
} catch (IllegalArgumentException e) {
104-
throw lookupChain.exceptionWithLookupStack(e);
117+
if (throwOnFailure) {
118+
throw lookupChain.exceptionWithLookupStack(e);
119+
}
120+
return null;
105121
} finally {
106122
lookupChain.pop(success);
107123
}
108124
}
109125

126+
/**
127+
* Build for the simple non-annotated type case.
128+
*/
129+
<T> JsonAdapter<T> build(Type type) {
130+
return build(type, type);
131+
}
132+
133+
/**
134+
* Build given type and annotations.
135+
*/
136+
<T> JsonAdapter<T> build(Type type, Object cacheKey) {
137+
return lookupAdapter(type, cacheKey, true);
138+
}
139+
110140
/**
111141
* A possibly-reentrant chain of lookups for JSON adapters.
112142
*

jsonb/src/main/java/io/avaje/jsonb/core/DJsonb.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,18 @@ public JsonAdapter<String> rawAdapter() {
212212
return RawAdapter.STR;
213213
}
214214

215+
@Override
216+
public boolean hasAdapter(Class<?> cls) {
217+
Type cacheKey = canonicalizeClass(requireNonNull(cls));
218+
return builder.hasAdapter(cacheKey);
219+
}
220+
221+
@Override
222+
public boolean hasAdapter(Type type) {
223+
type = removeSubtypeWildcard(canonicalize(requireNonNull(type)));
224+
return builder.hasAdapter(type);
225+
}
226+
215227
JsonReader objectReader(Object value) {
216228
return new ObjectJsonReader(value);
217229
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.avaje.jsonb.core;
2+
3+
import io.avaje.jsonb.Jsonb;
4+
import io.avaje.jsonb.Types;
5+
import org.junit.jupiter.api.DisplayName;
6+
import org.junit.jupiter.api.Test;
7+
8+
import java.lang.reflect.Type;
9+
import java.util.Optional;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
13+
class HasAdapterTest {
14+
15+
private final Jsonb jsonb = Jsonb.builder().build();
16+
17+
@Test
18+
@DisplayName("hasAdapter returns true for basic types and primitives")
19+
void hasAdapter_basicTypes_returnsTrue() {
20+
assertThat(jsonb.hasAdapter(String.class)).isTrue();
21+
assertThat(jsonb.hasAdapter(Integer.class)).isTrue();
22+
assertThat(jsonb.hasAdapter(Boolean.class)).isTrue();
23+
24+
assertThat(jsonb.hasAdapter(int.class)).isTrue();
25+
assertThat(jsonb.hasAdapter(boolean.class)).isTrue();
26+
assertThat(jsonb.hasAdapter(double.class)).isTrue();
27+
}
28+
29+
@Test
30+
@DisplayName("hasAdapter returns true for generic types like List, Map, and Optional")
31+
void hasAdapter_withGenericTypes_returnsTrue() {
32+
Type listOfString = Types.listOf(String.class);
33+
assertThat(jsonb.hasAdapter(listOfString)).isTrue();
34+
35+
Type mapOfStringToInteger = Types.mapOf(Integer.class);
36+
assertThat(jsonb.hasAdapter(mapOfStringToInteger)).isTrue();
37+
38+
Type optionalString = Types.newParameterizedType(Optional.class, String.class);
39+
assertThat(jsonb.hasAdapter(optionalString)).isTrue();
40+
}
41+
42+
@Test
43+
@DisplayName("hasAdapter returns false for types without adapters")
44+
void hasAdapter_whenAdapterNotExists_returnsFalse() {
45+
assertThat(jsonb.hasAdapter(UnknownClass.class)).isFalse();
46+
assertThat(jsonb.hasAdapter(SomeInterface.class)).isFalse();
47+
}
48+
49+
@Test
50+
@DisplayName("hasAdapter works correctly with cached adapters")
51+
void hasAdapter_withCachedAdapter_returnsTrue() {
52+
jsonb.type(String.class);
53+
assertThat(jsonb.hasAdapter(String.class)).isTrue();
54+
}
55+
56+
@Test
57+
@DisplayName("hasAdapter never throws exceptions, even for problematic types")
58+
void hasAdapter_doesNotThrowExceptions() {
59+
assertThat(jsonb.hasAdapter(UnknownClass.class)).isFalse();
60+
assertThat(jsonb.hasAdapter(SomeInterface.class)).isFalse();
61+
}
62+
63+
// Test classes
64+
private static class UnknownClass {
65+
private String value;
66+
}
67+
68+
private interface SomeInterface {
69+
String getValue();
70+
}
71+
}

0 commit comments

Comments
 (0)