Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions jsonb/src/main/java/io/avaje/jsonb/Jsonb.java
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ static Jsonb instance() {
* When using <code>Object.class</code> and reading <code>fromJson()</code> 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
*/
<T> JsonType<T> type(Class<T> cls);

Expand Down Expand Up @@ -279,6 +281,8 @@ static Jsonb instance() {
* When using <code>Object.class</code> and reading <code>fromJson()</code> 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
*/
<T> JsonType<T> type(Type type);

Expand Down Expand Up @@ -341,6 +345,8 @@ static Jsonb instance() {
*
* <p>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
*/
<T> JsonAdapter<T> adapter(Class<T> cls);

Expand All @@ -349,6 +355,8 @@ static Jsonb instance() {
*
* <p>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
*/
<T> JsonAdapter<T> adapter(Type type);

Expand All @@ -368,6 +376,22 @@ static Jsonb instance() {
*/
JsonAdapter<String> 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.
*/
Expand Down
52 changes: 41 additions & 11 deletions jsonb/src/main/java/io/avaje/jsonb/core/CoreAdapterBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,26 @@ <T> JsonAdapter<T> 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.
*/
<T> JsonAdapter<T> 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")
<T> JsonAdapter<T> build(Type type, Object cacheKey) {
private <T> JsonAdapter<T> lookupAdapter(Type type, Object cacheKey, boolean throwOnFailure) {
LookupChain lookupChain = lookupChainThreadLocal.get();
if (lookupChain == null) {
lookupChain = new LookupChain();
Expand All @@ -94,19 +103,40 @@ <T> JsonAdapter<T> 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.
*/
<T> JsonAdapter<T> build(Type type) {
return build(type, type);
}

/**
* Build given type and annotations.
*/
<T> JsonAdapter<T> build(Type type, Object cacheKey) {
return lookupAdapter(type, cacheKey, true);
}

/**
* A possibly-reentrant chain of lookups for JSON adapters.
*
Expand Down
12 changes: 12 additions & 0 deletions jsonb/src/main/java/io/avaje/jsonb/core/DJsonb.java
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,18 @@ public JsonAdapter<String> 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);
}
Expand Down
71 changes: 71 additions & 0 deletions jsonb/src/test/java/io/avaje/jsonb/core/HasAdapterTest.java
Original file line number Diff line number Diff line change
@@ -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();
}
}