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
2 changes: 2 additions & 0 deletions sdk-serde-jackson/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ dependencies {

implementation(project(":common"))

implementation(libs.log4j.api)

api(libs.jackson.databind)
implementation(libs.jackson.jsr310)
implementation(libs.jackson.core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
}

/**
Expand All @@ -99,7 +118,12 @@ public static <T> Serde<T> of(Class<T> clazz) {
public static <T> Serde<T> of(ObjectMapper mapper, Class<T> 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));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
Expand All @@ -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);
Expand All @@ -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;
}
Expand All @@ -64,11 +65,16 @@ public <T> Serde<T> create(Class<T> clazz) {
static <T> Serde<T> 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));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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. */
Expand Down
Loading