From 62f37119af352bd3b278dc3a63350eddc6ea7c70 Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Thu, 16 Jul 2026 11:37:29 +0200 Subject: [PATCH] Mitigation for incompatible victools version Fix #624 --- sdk-serde-jackson/build.gradle.kts | 2 + .../sdk/serde/jackson/JacksonSerdes.java | 102 +++++++++++------- .../serde/jackson/JacksonSerdeFactory.java | 14 ++- .../restate/serde/jackson/JacksonSerdes.java | 93 +++++++++------- 4 files changed, 131 insertions(+), 80 deletions(-) diff --git a/sdk-serde-jackson/build.gradle.kts b/sdk-serde-jackson/build.gradle.kts index 848239ebb..13890c0d9 100644 --- a/sdk-serde-jackson/build.gradle.kts +++ b/sdk-serde-jackson/build.gradle.kts @@ -12,6 +12,8 @@ dependencies { implementation(project(":common")) + implementation(libs.log4j.api) + api(libs.jackson.databind) implementation(libs.jackson.jsr310) implementation(libs.jackson.core) diff --git a/sdk-serde-jackson/src/main/java/dev/restate/sdk/serde/jackson/JacksonSerdes.java b/sdk-serde-jackson/src/main/java/dev/restate/sdk/serde/jackson/JacksonSerdes.java index b4a2caabb..515ea1be5 100644 --- a/sdk-serde-jackson/src/main/java/dev/restate/sdk/serde/jackson/JacksonSerdes.java +++ b/sdk-serde-jackson/src/main/java/dev/restate/sdk/serde/jackson/JacksonSerdes.java @@ -20,7 +20,10 @@ import dev.restate.serde.jackson.JacksonSerdeFactory; import java.io.IOException; import java.util.stream.StreamSupport; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; /** * @deprecated This will be removed in the next release, please check the individual methods for @@ -31,52 +34,68 @@ public final class JacksonSerdes { private JacksonSerdes() {} + private static final Logger LOG = LogManager.getLogger(JacksonSerdes.class); + private static final ObjectMapper defaultMapper; - private static final SchemaGenerator schemaGenerator; + + private static final @Nullable SchemaGenerator schemaGenerator; static { defaultMapper = new ObjectMapper(); // Find modules through SPI (e.g. jackson-datatype-jsr310) defaultMapper.findAndRegisterModules(); - JacksonModule module = - new JacksonModule( - JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.INLINE_TRANSFORMED_SUBTYPES); - SchemaGeneratorConfigBuilder configBuilder = - new SchemaGeneratorConfigBuilder( - defaultMapper, SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON) - .with(module); - - // Make sure we use `title` for types - configBuilder - .forTypesInGeneral() - .withTypeAttributeOverride( - (schema, scope, context) -> { - if (schema.isObject() - && !schema.hasNonNull( - SchemaKeyword.TAG_TITLE.forVersion( - context.getGeneratorConfig().getSchemaVersion()))) { - JsonNode typeKeyword = - schema.get( - SchemaKeyword.TAG_TYPE.forVersion( - context.getGeneratorConfig().getSchemaVersion())); - boolean isObjectSchema = - typeKeyword != null - && ((typeKeyword.isTextual() && "object".equals(typeKeyword.textValue())) - || (typeKeyword.isArray() - && StreamSupport.stream(typeKeyword.spliterator(), false) - .anyMatch( - el -> el.isTextual() && "object".equals(el.textValue())))); - if (isObjectSchema) { - schema.put( - SchemaKeyword.TAG_TITLE.forVersion( - context.getGeneratorConfig().getSchemaVersion()), - scope.getSimpleTypeDescription()); - } - } - }); + schemaGenerator = buildSchemaGenerator(defaultMapper); + } - schemaGenerator = new SchemaGenerator(configBuilder.build()); + private static @Nullable SchemaGenerator buildSchemaGenerator(ObjectMapper mapper) { + try { + JacksonModule module = + new JacksonModule( + JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, + JacksonOption.INLINE_TRANSFORMED_SUBTYPES); + SchemaGeneratorConfigBuilder configBuilder = + new SchemaGeneratorConfigBuilder( + mapper, SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON) + .with(module); + + // Make sure we use `title` for types + configBuilder + .forTypesInGeneral() + .withTypeAttributeOverride( + (schema, scope, context) -> { + if (schema.isObject() + && !schema.hasNonNull( + SchemaKeyword.TAG_TITLE.forVersion( + context.getGeneratorConfig().getSchemaVersion()))) { + JsonNode typeKeyword = + schema.get( + SchemaKeyword.TAG_TYPE.forVersion( + context.getGeneratorConfig().getSchemaVersion())); + boolean isObjectSchema = + typeKeyword != null + && ((typeKeyword.isTextual() && "object".equals(typeKeyword.textValue())) + || (typeKeyword.isArray() + && StreamSupport.stream(typeKeyword.spliterator(), false) + .anyMatch( + el -> + el.isTextual() && "object".equals(el.textValue())))); + if (isObjectSchema) { + schema.put( + SchemaKeyword.TAG_TITLE.forVersion( + context.getGeneratorConfig().getSchemaVersion()), + scope.getSimpleTypeDescription()); + } + } + }); + + return new SchemaGenerator(configBuilder.build()); + } catch (LinkageError t) { + LOG.warn( + "Cannot initialize the Jackson JSON Schema generator due to an incompatible version of com.github.victools:jsonschema-generator.", + t); + return null; + } } /** @@ -99,7 +118,12 @@ public static Serde of(Class clazz) { public static Serde of(ObjectMapper mapper, Class clazz) { return new Serde<>() { @Override - public Schema jsonSchema() { + public @Nullable Schema jsonSchema() { + // schemaGenerator is null when an incompatible victools version is on the classpath, + // see #buildSchemaGenerator. + if (schemaGenerator == null) { + return null; + } return new JsonSchema(schemaGenerator.generateSchema(clazz)); } diff --git a/sdk-serde-jackson/src/main/java/dev/restate/serde/jackson/JacksonSerdeFactory.java b/sdk-serde-jackson/src/main/java/dev/restate/serde/jackson/JacksonSerdeFactory.java index adcb1d8bd..5a5719feb 100644 --- a/sdk-serde-jackson/src/main/java/dev/restate/serde/jackson/JacksonSerdeFactory.java +++ b/sdk-serde-jackson/src/main/java/dev/restate/serde/jackson/JacksonSerdeFactory.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.lang.reflect.Type; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; /** * This class implements {@link SerdeFactory} using Jackson's {@link ObjectMapper}. @@ -35,7 +36,7 @@ public class JacksonSerdeFactory implements SerdeFactory { public static final JacksonSerdeFactory DEFAULT = new JacksonSerdeFactory(); private final ObjectMapper mapper; - private final SchemaGenerator schemaGenerator; + private final @Nullable SchemaGenerator schemaGenerator; public JacksonSerdeFactory() { this(JacksonSerdes.defaultMapper); @@ -45,7 +46,7 @@ public JacksonSerdeFactory(ObjectMapper mapper) { this(mapper, JacksonSerdes.schemaGenerator); } - public JacksonSerdeFactory(ObjectMapper mapper, SchemaGenerator schemaGenerator) { + public JacksonSerdeFactory(ObjectMapper mapper, @Nullable SchemaGenerator schemaGenerator) { this.mapper = mapper; this.schemaGenerator = schemaGenerator; } @@ -64,11 +65,16 @@ public Serde create(Class clazz) { static Serde create( JavaType constructedType, Type originalType, - SchemaGenerator schemaGenerator, + @Nullable SchemaGenerator schemaGenerator, ObjectMapper mapper) { return new Serde<>() { @Override - public Schema jsonSchema() { + public @Nullable Schema jsonSchema() { + // schemaGenerator is null when an incompatible victools version is on the classpath, + // see JacksonSerdes#buildSchemaGenerator. + if (schemaGenerator == null) { + return null; + } return new Serde.JsonSchema(schemaGenerator.generateSchema(originalType)); } diff --git a/sdk-serde-jackson/src/main/java/dev/restate/serde/jackson/JacksonSerdes.java b/sdk-serde-jackson/src/main/java/dev/restate/serde/jackson/JacksonSerdes.java index 9b52dc714..02b3beac4 100644 --- a/sdk-serde-jackson/src/main/java/dev/restate/serde/jackson/JacksonSerdes.java +++ b/sdk-serde-jackson/src/main/java/dev/restate/serde/jackson/JacksonSerdes.java @@ -16,6 +16,9 @@ import com.github.victools.jsonschema.module.jackson.JacksonOption; import dev.restate.serde.Serde; import java.util.stream.StreamSupport; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jspecify.annotations.Nullable; /** * {@link Serde} implementations for Jackson. @@ -42,52 +45,68 @@ public final class JacksonSerdes { private JacksonSerdes() {} + private static final Logger LOG = LogManager.getLogger(JacksonSerdes.class); + static final ObjectMapper defaultMapper; - static final SchemaGenerator schemaGenerator; + + static final @Nullable SchemaGenerator schemaGenerator; static { defaultMapper = new ObjectMapper(); // Find modules through SPI (e.g. jackson-datatype-jsr310) defaultMapper.findAndRegisterModules(); - JacksonModule module = - new JacksonModule( - JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.INLINE_TRANSFORMED_SUBTYPES); - SchemaGeneratorConfigBuilder configBuilder = - new SchemaGeneratorConfigBuilder( - defaultMapper, SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON) - .with(module); - - // Make sure we use `title` for types - configBuilder - .forTypesInGeneral() - .withTypeAttributeOverride( - (schema, scope, context) -> { - if (schema.isObject() - && !schema.hasNonNull( - SchemaKeyword.TAG_TITLE.forVersion( - context.getGeneratorConfig().getSchemaVersion()))) { - JsonNode typeKeyword = - schema.get( - SchemaKeyword.TAG_TYPE.forVersion( - context.getGeneratorConfig().getSchemaVersion())); - boolean isObjectSchema = - typeKeyword != null - && ((typeKeyword.isTextual() && "object".equals(typeKeyword.textValue())) - || (typeKeyword.isArray() - && StreamSupport.stream(typeKeyword.spliterator(), false) - .anyMatch( - el -> el.isTextual() && "object".equals(el.textValue())))); - if (isObjectSchema) { - schema.put( - SchemaKeyword.TAG_TITLE.forVersion( - context.getGeneratorConfig().getSchemaVersion()), - scope.getSimpleTypeDescription()); + schemaGenerator = buildSchemaGenerator(defaultMapper); + } + + private static @Nullable SchemaGenerator buildSchemaGenerator(ObjectMapper mapper) { + try { + JacksonModule module = + new JacksonModule( + JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, + JacksonOption.INLINE_TRANSFORMED_SUBTYPES); + SchemaGeneratorConfigBuilder configBuilder = + new SchemaGeneratorConfigBuilder( + mapper, SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON) + .with(module); + + // Make sure we use `title` for types + configBuilder + .forTypesInGeneral() + .withTypeAttributeOverride( + (schema, scope, context) -> { + if (schema.isObject() + && !schema.hasNonNull( + SchemaKeyword.TAG_TITLE.forVersion( + context.getGeneratorConfig().getSchemaVersion()))) { + JsonNode typeKeyword = + schema.get( + SchemaKeyword.TAG_TYPE.forVersion( + context.getGeneratorConfig().getSchemaVersion())); + boolean isObjectSchema = + typeKeyword != null + && ((typeKeyword.isTextual() && "object".equals(typeKeyword.textValue())) + || (typeKeyword.isArray() + && StreamSupport.stream(typeKeyword.spliterator(), false) + .anyMatch( + el -> + el.isTextual() && "object".equals(el.textValue())))); + if (isObjectSchema) { + schema.put( + SchemaKeyword.TAG_TITLE.forVersion( + context.getGeneratorConfig().getSchemaVersion()), + scope.getSimpleTypeDescription()); + } } - } - }); + }); - schemaGenerator = new SchemaGenerator(configBuilder.build()); + return new SchemaGenerator(configBuilder.build()); + } catch (LinkageError t) { + LOG.warn( + "Cannot initialize the Jackson JSON Schema generator due to an incompatible version of com.github.victools:jsonschema-generator.", + t); + return null; + } } /** Serialize/Deserialize class using the default object mapper. */