|
| 1 | +package com.sap.ai.sdk.orchestration; |
| 2 | + |
| 3 | +import com.fasterxml.jackson.annotation.JsonSubTypes; |
| 4 | +import com.fasterxml.jackson.core.JsonParser; |
| 5 | +import com.fasterxml.jackson.databind.DeserializationContext; |
| 6 | +import com.fasterxml.jackson.databind.JsonMappingException; |
| 7 | +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; |
| 8 | +import java.io.IOException; |
| 9 | +import java.util.ArrayList; |
| 10 | +import java.util.List; |
| 11 | + |
| 12 | +/** |
| 13 | + * Handles polymorphic deserialization for a base class or interface. |
| 14 | + * |
| 15 | + * <p>This deserializer attempts to deserialize JSON into a subtype of a base class or interface. |
| 16 | + * Subtypes are either discovered using the {@link JsonSubTypes} annotation or provided explicitly. |
| 17 | + * If deserialization fails for all candidates, a {@link JsonMappingException} is thrown with |
| 18 | + * suppressed exceptions. |
| 19 | + * |
| 20 | + * @param <T> The base type for deserialization. |
| 21 | + */ |
| 22 | +public class PolymorphicFallbackDeserializer<T> extends StdDeserializer<T> { |
| 23 | + |
| 24 | + private final List<Class<? extends T>> candidates; |
| 25 | + |
| 26 | + /** |
| 27 | + * Constructs the deserializer using the {@link JsonSubTypes} annotation. |
| 28 | + * |
| 29 | + * @param baseClass The base class or interface to be resolved. |
| 30 | + * @throws IllegalStateException If no subtypes are found. |
| 31 | + */ |
| 32 | + protected PolymorphicFallbackDeserializer(Class<T> baseClass) { |
| 33 | + super(baseClass); |
| 34 | + |
| 35 | + final var subTypes = baseClass.getAnnotation(JsonSubTypes.class); |
| 36 | + if (subTypes == null || subTypes.value().length == 0) { |
| 37 | + throw new IllegalStateException("No subtypes found for " + baseClass.getName()); |
| 38 | + } |
| 39 | + |
| 40 | + candidates = new ArrayList<>(); |
| 41 | + for (final var subType : subTypes.value()) { |
| 42 | + candidates.add((Class<? extends T>) subType.value()); |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + /** |
| 47 | + * Constructs the deserializer with an explicit list of candidate types. |
| 48 | + * |
| 49 | + * @param baseClass The base class or interface to be resolved. |
| 50 | + * @param candidates A list of candidate classes to try deserialization. |
| 51 | + */ |
| 52 | + protected PolymorphicFallbackDeserializer( |
| 53 | + Class<T> baseClass, List<Class<? extends T>> candidates) { |
| 54 | + super(baseClass); |
| 55 | + this.candidates = candidates; |
| 56 | + } |
| 57 | + |
| 58 | + /** |
| 59 | + * Deserializes the JSON into the first matching candidate type. |
| 60 | + * |
| 61 | + * @param jsonParser The parser providing the JSON. |
| 62 | + * @param deserializationContext The deserialization context. |
| 63 | + * @return The deserialized object of a matching candidate type. |
| 64 | + * @throws JsonMappingException If deserialization fails for all candidates. |
| 65 | + * @throws IOException If an I/O error occurs. |
| 66 | + */ |
| 67 | + @Override |
| 68 | + public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) |
| 69 | + throws IOException { |
| 70 | + |
| 71 | + final var root = jsonParser.readValueAsTree(); |
| 72 | + final var throwable = |
| 73 | + JsonMappingException.from( |
| 74 | + jsonParser, |
| 75 | + "PolymorphicFallbackDeserializer failed to deserialize " + handledType().getName()); |
| 76 | + |
| 77 | + for (final var candidate : candidates) { |
| 78 | + try { |
| 79 | + return jsonParser.getCodec().treeToValue(root, candidate); |
| 80 | + } catch (JsonMappingException e) { |
| 81 | + throwable.addSuppressed(e); |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + throw throwable; |
| 86 | + } |
| 87 | +} |
0 commit comments