Skip to content

Commit 2799771

Browse files
committed
Improve PolymorphicFallbackDeserializer
- Generalize test case - add @SInCE and @beta annotation on custom deserializer (public)
1 parent e7406f5 commit 2799771

File tree

4 files changed

+65
-42
lines changed

4 files changed

+65
-42
lines changed

orchestration/src/main/java/com/sap/ai/sdk/orchestration/OrchestrationClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public class OrchestrationClient {
5656
new SimpleModule()
5757
.addDeserializer(
5858
ChatMessagesInner.class,
59-
new PolymorphicFallbackDeserializer<>(ChatMessagesInner.class))
59+
PolymorphicFallbackDeserializer.fromJsonSubTypes(ChatMessagesInner.class))
6060
.setMixInAnnotation(ChatMessagesInner.class, JacksonMixins.NoneTypeInfoMixin.class);
6161
JACKSON.registerModule(module);
6262
}

orchestration/src/main/java/com/sap/ai/sdk/orchestration/PolymorphicFallbackDeserializer.java

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.fasterxml.jackson.databind.DeserializationContext;
66
import com.fasterxml.jackson.databind.JsonDeserializer;
77
import com.fasterxml.jackson.databind.JsonMappingException;
8+
import com.google.common.annotations.Beta;
89
import java.io.IOException;
910
import java.util.ArrayList;
1011
import java.util.List;
@@ -18,43 +19,53 @@
1819
* explicitly. If deserialization fails for all candidates, a {@link JsonMappingException} is thrown
1920
* with suppressed exceptions.
2021
*
22+
* @since 1.2.0
2123
* @param <T> The base type for deserialization.
2224
*/
25+
@Beta
2326
public class PolymorphicFallbackDeserializer<T> extends JsonDeserializer<T> {
2427

2528
@Nonnull private final List<Class<? extends T>> candidates;
26-
Class<T> baseClass;
29+
@Nonnull private final Class<T> baseClass;
30+
31+
private PolymorphicFallbackDeserializer(
32+
@Nonnull final Class<T> baseClass, @Nonnull final List<Class<? extends T>> candidates) {
33+
this.baseClass = baseClass;
34+
this.candidates = candidates;
35+
}
2736

2837
/**
29-
* Constructs the deserializer using the {@link JsonSubTypes} annotation.
38+
* Constructs the deserializer using candidates inferred from the {@link JsonSubTypes} annotation.
3039
*
3140
* @param baseClass The base class or interface to be resolved.
3241
* @throws IllegalStateException If no subtypes are found.
3342
*/
34-
protected PolymorphicFallbackDeserializer(@Nonnull final Class<T> baseClass) {
35-
this.baseClass = baseClass;
36-
43+
@Nonnull
44+
protected static <T> PolymorphicFallbackDeserializer<T> fromJsonSubTypes(
45+
@Nonnull final Class<T> baseClass) {
3746
final var subTypes = baseClass.getAnnotation(JsonSubTypes.class);
3847
if (subTypes == null || subTypes.value().length == 0) {
3948
throw new IllegalStateException("No subtypes found for " + baseClass.getName());
4049
}
4150

42-
candidates = new ArrayList<>();
51+
final var candidates = new ArrayList<Class<? extends T>>();
4352
for (final var subType : subTypes.value()) {
4453
candidates.add((Class<? extends T>) subType.value());
4554
}
55+
56+
return new PolymorphicFallbackDeserializer<>(baseClass, candidates);
4657
}
4758

4859
/**
49-
* Constructs the deserializer with an explicit list of candidate types.
60+
* Constructs the deserializer with an explicit given list of candidate types.
5061
*
5162
* @param baseClass The base class or interface to be resolved.
5263
* @param candidates A list of candidate classes to try deserialization.
5364
*/
54-
protected PolymorphicFallbackDeserializer(
65+
@Nonnull
66+
protected static <T> PolymorphicFallbackDeserializer<T> fromCandidates(
5567
@Nonnull final Class<T> baseClass, @Nonnull final List<Class<? extends T>> candidates) {
56-
this.baseClass = baseClass;
57-
this.candidates = candidates;
68+
return new PolymorphicFallbackDeserializer<>(baseClass, candidates);
5869
}
5970

6071
/**
@@ -74,19 +85,25 @@ public T deserialize(
7485
throws IOException {
7586

7687
final var root = jsonParser.readValueAsTree();
77-
final var throwable =
78-
JsonMappingException.from(
79-
jsonParser,
80-
"PolymorphicFallbackDeserializer failed to deserialize " + this.baseClass.getName());
88+
final var suppressed = new ArrayList<JsonMappingException>();
8189

8290
for (final var candidate : candidates) {
8391
try {
8492
return jsonParser.getCodec().treeToValue(root, candidate);
8593
} catch (JsonMappingException e) {
86-
throwable.addSuppressed(e);
94+
suppressed.add(e);
8795
}
8896
}
8997

90-
throw throwable;
98+
final var aggregateException =
99+
JsonMappingException.from(
100+
jsonParser,
101+
"PolymorphicFallbackDeserializer failed to deserialize "
102+
+ this.baseClass.getName()
103+
+ ". Attempted candidates: "
104+
+ candidates.stream().map(Class::getName).toList());
105+
106+
suppressed.forEach(aggregateException::addSuppressed);
107+
throw aggregateException;
91108
}
92109
}

orchestration/src/test/java/com/sap/ai/sdk/orchestration/OrchestrationUnitTest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,10 +753,12 @@ void testOrchestrationChatResponseWithMultiChatMessage() {
753753
var module = new SimpleModule();
754754
module.setMixInAnnotation(LLMModuleResult.class, JacksonMixins.NoneTypeInfoMixin.class);
755755
module.addDeserializer(
756-
LLMModuleResult.class, new PolymorphicFallbackDeserializer<>(LLMModuleResult.class));
756+
LLMModuleResult.class,
757+
PolymorphicFallbackDeserializer.fromJsonSubTypes(LLMModuleResult.class));
757758
module.setMixInAnnotation(ChatMessagesInner.class, JacksonMixins.NoneTypeInfoMixin.class);
758759
module.addDeserializer(
759-
ChatMessagesInner.class, new PolymorphicFallbackDeserializer<>(ChatMessagesInner.class));
760+
ChatMessagesInner.class,
761+
PolymorphicFallbackDeserializer.fromJsonSubTypes(ChatMessagesInner.class));
760762

761763
var orchestrationChatResponse =
762764
new OrchestrationChatResponse(

orchestration/src/test/java/com/sap/ai/sdk/orchestration/PolymorphicFallbackDeserializerTest.java

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
import com.fasterxml.jackson.databind.JsonMappingException;
66
import com.fasterxml.jackson.databind.json.JsonMapper;
77
import com.fasterxml.jackson.databind.module.SimpleModule;
8-
import com.sap.ai.sdk.orchestration.model.ChatMessage;
9-
import com.sap.ai.sdk.orchestration.model.ChatMessagesInner;
108
import java.util.List;
119
import lombok.SneakyThrows;
1210
import org.junit.jupiter.api.Test;
@@ -17,48 +15,54 @@ class PolymorphicFallbackDeserializerTest {
1715
@Test
1816
void testValueTypeResolutionFailure() {
1917

20-
final String multiChatMessageJson =
18+
interface AbstractParent {}
19+
20+
class KnownCandidate implements AbstractParent {
21+
String content;
22+
}
23+
24+
class UnknownCandidate implements AbstractParent {
25+
List<String> content;
26+
}
27+
28+
final String unknownCandidateJson =
2129
"""
2230
{
23-
"role": "user",
2431
"content": [
25-
{
26-
"type": "text",
27-
"text": "What’s in this image?"
28-
},
29-
{
30-
"type": "image_url",
31-
"image_url": {
32-
"url": "https://sample.page.org/an-image.jpg"
33-
}
34-
}
32+
"Sentence one",
33+
"Sentence two"
3534
]
3635
}
3736
""";
3837

3938
final var module =
4039
new SimpleModule()
4140
.addDeserializer(
42-
ChatMessagesInner.class,
43-
new PolymorphicFallbackDeserializer<>(
44-
ChatMessagesInner.class, List.of(ChatMessage.class)))
45-
.setMixInAnnotation(ChatMessagesInner.class, JacksonMixins.NoneTypeInfoMixin.class);
41+
AbstractParent.class,
42+
PolymorphicFallbackDeserializer.fromCandidates(
43+
AbstractParent.class, List.of(KnownCandidate.class)));
4644

4745
final var mapper = new JsonMapper().registerModule(module);
4846

49-
assertThatThrownBy(() -> mapper.readValue(multiChatMessageJson, ChatMessagesInner.class))
47+
assertThatThrownBy(() -> mapper.readValue(unknownCandidateJson, AbstractParent.class))
5048
.isInstanceOf(JsonMappingException.class)
5149
.hasMessageContaining(
5250
"PolymorphicFallbackDeserializer failed to deserialize "
53-
+ ChatMessagesInner.class.getName());
51+
+ AbstractParent.class.getName()
52+
+ ". Attempted candidates: "
53+
+ List.of(KnownCandidate.class.getName()));
5454
}
5555

5656
@Test
5757
void testMissingSubtypeAnnotation() {
58-
interface NoSubTypesInterface {}
58+
interface NoJsonSubTypeAnnotationInterface {}
5959

60-
assertThatThrownBy(() -> new PolymorphicFallbackDeserializer<>(NoSubTypesInterface.class))
60+
assertThatThrownBy(
61+
() ->
62+
PolymorphicFallbackDeserializer.fromJsonSubTypes(
63+
NoJsonSubTypeAnnotationInterface.class))
6164
.isInstanceOf(IllegalStateException.class)
62-
.hasMessageContaining("No subtypes found for " + NoSubTypesInterface.class.getName());
65+
.hasMessageContaining(
66+
"No subtypes found for " + NoJsonSubTypeAnnotationInterface.class.getName());
6367
}
6468
}

0 commit comments

Comments
 (0)