Skip to content

Commit 4e6c65f

Browse files
Add Json Schema for Req/Res types (#395)
* Add RichSchema to propagate Json Schema * Propagate schemas in EndpointManifest * Cleanup counter example types, bring in jackson parameter names, fix logging
1 parent 2b2f533 commit 4e6c65f

File tree

9 files changed

+161
-26
lines changed

9 files changed

+161
-26
lines changed

examples/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ dependencies {
2121

2222
implementation(platform(jacksonLibs.jackson.bom))
2323
implementation(jacksonLibs.jackson.jsr310)
24+
implementation(jacksonLibs.jackson.parameter.names)
2425

2526
implementation(kotlinLibs.kotlinx.coroutines)
2627
implementation(kotlinLibs.kotlinx.serialization.core)
@@ -40,3 +41,5 @@ application {
4041
tasks.withType<Jar> { this.enabled = false }
4142

4243
tasks.withType<ShadowJar> { transform(ServiceFileTransformer::class.java) }
44+
45+
tasks.withType<JavaCompile> { options.compilerArgs.add("-parameters") }

examples/src/main/java/my/restate/sdk/examples/Counter.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,20 @@ public void reset(ObjectContext ctx) {
3232
}
3333

3434
@Handler
35-
public void add(ObjectContext ctx, Long request) {
35+
public void add(ObjectContext ctx, long request) {
3636
long currentValue = ctx.get(TOTAL).orElse(0L);
3737
long newValue = currentValue + request;
3838
ctx.set(TOTAL, newValue);
3939
}
4040

4141
@Shared
4242
@Handler
43-
public Long get(SharedObjectContext ctx) {
43+
public long get(SharedObjectContext ctx) {
4444
return ctx.get(TOTAL).orElse(0L);
4545
}
4646

4747
@Handler
48-
public CounterUpdateResult getAndAdd(ObjectContext ctx, Long request) {
48+
public CounterUpdateResult getAndAdd(ObjectContext ctx, long request) {
4949
LOG.info("Invoked get and add with {}", request);
5050

5151
long currentValue = ctx.get(TOTAL).orElse(0L);
@@ -60,19 +60,19 @@ public static void main(String[] args) {
6060
}
6161

6262
public static class CounterUpdateResult {
63-
private final Long newValue;
64-
private final Long oldValue;
63+
private final long newValue;
64+
private final long oldValue;
6565

66-
public CounterUpdateResult(Long newValue, Long oldValue) {
66+
public CounterUpdateResult(long newValue, long oldValue) {
6767
this.newValue = newValue;
6868
this.oldValue = oldValue;
6969
}
7070

71-
public Long getNewValue() {
71+
public long getNewValue() {
7272
return newValue;
7373
}
7474

75-
public Long getOldValue() {
75+
public long getOldValue() {
7676
return oldValue;
7777
}
7878
}

examples/src/main/resources/log4j2.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ appender.console.filter.replay.0.type = KeyValuePair
1515
appender.console.filter.replay.0.key = restateInvocationStatus
1616
appender.console.filter.replay.0.value = REPLAYING
1717

18-
# Restate logs to debug level
18+
# Restate logs to info level
1919
logger.app.name = dev.restate
20-
logger.app.level = error
20+
logger.app.level = info
2121
logger.app.additivity = false
2222
logger.app.appenderRef.console.ref = consoleLogger
2323

sdk-api/src/main/java/dev/restate/sdk/JsonSerdes.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
import com.fasterxml.jackson.core.JsonGenerator;
1313
import com.fasterxml.jackson.core.JsonParser;
1414
import com.fasterxml.jackson.core.JsonToken;
15+
import dev.restate.sdk.common.RichSerde;
1516
import dev.restate.sdk.common.Serde;
1617
import dev.restate.sdk.common.function.ThrowingBiConsumer;
1718
import dev.restate.sdk.common.function.ThrowingFunction;
1819
import java.io.ByteArrayInputStream;
1920
import java.io.ByteArrayOutputStream;
2021
import java.io.IOException;
22+
import java.util.Map;
2123
import org.jspecify.annotations.NonNull;
2224

2325
/**
@@ -32,6 +34,7 @@ private JsonSerdes() {}
3234
/** {@link Serde} for {@link String}. This writes and reads {@link String} as JSON value. */
3335
public static Serde<@NonNull String> STRING =
3436
usingJackson(
37+
"string",
3538
JsonGenerator::writeString,
3639
p -> {
3740
if (p.nextToken() != JsonToken.VALUE_STRING) {
@@ -44,6 +47,7 @@ private JsonSerdes() {}
4447
/** {@link Serde} for {@link Boolean}. This writes and reads {@link Boolean} as JSON value. */
4548
public static Serde<@NonNull Boolean> BOOLEAN =
4649
usingJackson(
50+
"boolean",
4751
JsonGenerator::writeBoolean,
4852
p -> {
4953
p.nextToken();
@@ -53,6 +57,7 @@ private JsonSerdes() {}
5357
/** {@link Serde} for {@link Byte}. This writes and reads {@link Byte} as JSON value. */
5458
public static Serde<@NonNull Byte> BYTE =
5559
usingJackson(
60+
"number",
5661
JsonGenerator::writeNumber,
5762
p -> {
5863
p.nextToken();
@@ -62,6 +67,7 @@ private JsonSerdes() {}
6267
/** {@link Serde} for {@link Short}. This writes and reads {@link Short} as JSON value. */
6368
public static Serde<@NonNull Short> SHORT =
6469
usingJackson(
70+
"number",
6571
JsonGenerator::writeNumber,
6672
p -> {
6773
p.nextToken();
@@ -71,6 +77,7 @@ private JsonSerdes() {}
7177
/** {@link Serde} for {@link Integer}. This writes and reads {@link Integer} as JSON value. */
7278
public static Serde<@NonNull Integer> INT =
7379
usingJackson(
80+
"number",
7481
JsonGenerator::writeNumber,
7582
p -> {
7683
p.nextToken();
@@ -80,6 +87,7 @@ private JsonSerdes() {}
8087
/** {@link Serde} for {@link Long}. This writes and reads {@link Long} as JSON value. */
8188
public static Serde<@NonNull Long> LONG =
8289
usingJackson(
90+
"number",
8391
JsonGenerator::writeNumber,
8492
p -> {
8593
p.nextToken();
@@ -89,6 +97,7 @@ private JsonSerdes() {}
8997
/** {@link Serde} for {@link Float}. This writes and reads {@link Float} as JSON value. */
9098
public static Serde<@NonNull Float> FLOAT =
9199
usingJackson(
100+
"number",
92101
JsonGenerator::writeNumber,
93102
p -> {
94103
p.nextToken();
@@ -98,6 +107,7 @@ private JsonSerdes() {}
98107
/** {@link Serde} for {@link Double}. This writes and reads {@link Double} as JSON value. */
99108
public static Serde<@NonNull Double> DOUBLE =
100109
usingJackson(
110+
"number",
101111
JsonGenerator::writeNumber,
102112
p -> {
103113
p.nextToken();
@@ -109,9 +119,16 @@ private JsonSerdes() {}
109119
private static final JsonFactory JSON_FACTORY = new JsonFactory();
110120

111121
private static <T extends @NonNull Object> Serde<T> usingJackson(
122+
String type,
112123
ThrowingBiConsumer<JsonGenerator, T> serializer,
113124
ThrowingFunction<JsonParser, T> deserializer) {
114-
return new Serde<>() {
125+
return new RichSerde<>() {
126+
127+
@Override
128+
public Object jsonSchema() {
129+
return Map.of("type", type);
130+
}
131+
115132
@Override
116133
public byte[] serialize(T value) {
117134
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) 2023 - Restate Software, Inc., Restate GmbH
2+
//
3+
// This file is part of the Restate Java SDK,
4+
// which is released under the MIT license.
5+
//
6+
// You can find a copy of the license in file LICENSE in the root
7+
// directory of this repository or package, or at
8+
// https://github.com/restatedev/sdk-java/blob/main/LICENSE
9+
package dev.restate.sdk.common;
10+
11+
import java.nio.ByteBuffer;
12+
import org.jspecify.annotations.Nullable;
13+
14+
/**
15+
* Richer version of {@link Serde} containing schema information.
16+
*
17+
* <p>You can create one using {@link #withSchema(Object, Serde)}.
18+
*/
19+
public interface RichSerde<T extends @Nullable Object> extends Serde<T> {
20+
21+
/**
22+
* @return a Draft 2020-12 Json Schema
23+
*/
24+
Object jsonSchema();
25+
26+
static <T> RichSerde<T> withSchema(Object jsonSchema, Serde<T> inner) {
27+
return new RichSerde<T>() {
28+
@Override
29+
public byte[] serialize(T value) {
30+
return inner.serialize(value);
31+
}
32+
33+
@Override
34+
public ByteBuffer serializeToByteBuffer(T value) {
35+
return inner.serializeToByteBuffer(value);
36+
}
37+
38+
@Override
39+
public T deserialize(ByteBuffer byteBuffer) {
40+
return inner.deserialize(byteBuffer);
41+
}
42+
43+
@Override
44+
public T deserialize(byte[] value) {
45+
return inner.deserialize(value);
46+
}
47+
48+
@Override
49+
public String contentType() {
50+
return inner.contentType();
51+
}
52+
53+
@Override
54+
public Object jsonSchema() {
55+
return jsonSchema;
56+
}
57+
};
58+
}
59+
}

sdk-core/src/main/java/dev/restate/sdk/core/EndpointManifest.java

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
import static dev.restate.sdk.core.ServiceProtocol.*;
1212

1313
import dev.restate.sdk.common.HandlerType;
14+
import dev.restate.sdk.common.RichSerde;
1415
import dev.restate.sdk.common.ServiceType;
1516
import dev.restate.sdk.common.syscalls.HandlerDefinition;
1617
import dev.restate.sdk.common.syscalls.HandlerSpecification;
1718
import dev.restate.sdk.common.syscalls.ServiceDefinition;
1819
import dev.restate.sdk.core.manifest.*;
20+
import java.util.Objects;
1921
import java.util.stream.Collectors;
2022
import java.util.stream.Stream;
2123

@@ -68,24 +70,45 @@ private static Service.Ty convertServiceType(ServiceType serviceType) {
6870

6971
private static Handler convertHandler(HandlerDefinition<?, ?, ?> handler) {
7072
HandlerSpecification<?, ?> spec = handler.getSpec();
73+
return new Handler()
74+
.withName(spec.getName())
75+
.withTy(convertHandlerType(spec.getHandlerType()))
76+
.withInput(convertHandlerInput(spec))
77+
.withOutput(convertHandlerOutput(spec));
78+
}
79+
80+
private static Input convertHandlerInput(HandlerSpecification<?, ?> spec) {
7181
String acceptContentType =
7282
spec.getAcceptContentType() != null
7383
? spec.getAcceptContentType()
7484
: spec.getRequestSerde().contentType();
7585

76-
return new Handler()
77-
.withName(spec.getName())
78-
.withTy(convertHandlerType(spec.getHandlerType()))
79-
.withInput(
80-
acceptContentType == null
81-
? EMPTY_INPUT
82-
: new Input().withRequired(true).withContentType(acceptContentType))
83-
.withOutput(
84-
spec.getResponseSerde().contentType() == null
85-
? EMPTY_OUTPUT
86-
: new Output()
87-
.withContentType(spec.getResponseSerde().contentType())
88-
.withSetContentTypeIfEmpty(false));
86+
Input input =
87+
acceptContentType == null
88+
? EMPTY_INPUT
89+
: new Input().withRequired(true).withContentType(acceptContentType);
90+
91+
if (spec.getRequestSerde() instanceof RichSerde) {
92+
input.setJsonSchema(
93+
Objects.requireNonNull(((RichSerde<?>) spec.getRequestSerde()).jsonSchema()));
94+
}
95+
return input;
96+
}
97+
98+
private static Output convertHandlerOutput(HandlerSpecification<?, ?> spec) {
99+
Output output =
100+
spec.getResponseSerde().contentType() == null
101+
? EMPTY_OUTPUT
102+
: new Output()
103+
.withContentType(spec.getResponseSerde().contentType())
104+
.withSetContentTypeIfEmpty(false);
105+
106+
if (spec.getResponseSerde() instanceof RichSerde) {
107+
output.setJsonSchema(
108+
Objects.requireNonNull(((RichSerde<?>) spec.getResponseSerde()).jsonSchema()));
109+
}
110+
111+
return output;
89112
}
90113

91114
private static Handler.Ty convertHandlerType(HandlerType handlerType) {

sdk-serde-jackson/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ dependencies {
1414
api(jacksonLibs.jackson.databind)
1515
implementation(jacksonLibs.jackson.core)
1616

17+
implementation("com.github.victools:jsonschema-generator:4.37.0")
18+
implementation("com.github.victools:jsonschema-module-jackson:4.37.0")
19+
1720
testImplementation(testingLibs.junit.jupiter)
1821
testImplementation(testingLibs.assertj)
1922

sdk-serde-jackson/src/main/java/dev/restate/sdk/serde/jackson/JacksonSerdes.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,13 @@
1111
import com.fasterxml.jackson.core.JsonProcessingException;
1212
import com.fasterxml.jackson.core.type.TypeReference;
1313
import com.fasterxml.jackson.databind.ObjectMapper;
14+
import com.github.victools.jsonschema.generator.*;
15+
import com.github.victools.jsonschema.module.jackson.JacksonModule;
16+
import com.github.victools.jsonschema.module.jackson.JacksonOption;
17+
import dev.restate.sdk.common.RichSerde;
1418
import dev.restate.sdk.common.Serde;
1519
import java.io.IOException;
20+
import org.jspecify.annotations.Nullable;
1621

1722
/**
1823
* {@link Serde} implementations for Jackson.
@@ -40,11 +45,21 @@ public final class JacksonSerdes {
4045
private JacksonSerdes() {}
4146

4247
private static final ObjectMapper defaultMapper;
48+
private static final SchemaGenerator schemaGenerator;
4349

4450
static {
4551
defaultMapper = new ObjectMapper();
4652
// Find modules through SPI (e.g. jackson-datatype-jsr310)
4753
defaultMapper.findAndRegisterModules();
54+
55+
JacksonModule module =
56+
new JacksonModule(
57+
JacksonOption.RESPECT_JSONPROPERTY_REQUIRED, JacksonOption.INLINE_TRANSFORMED_SUBTYPES);
58+
SchemaGeneratorConfigBuilder configBuilder =
59+
new SchemaGeneratorConfigBuilder(
60+
defaultMapper, SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON)
61+
.with(module);
62+
schemaGenerator = new SchemaGenerator(configBuilder.build());
4863
}
4964

5065
/** Serialize/Deserialize class using the default object mapper. */
@@ -54,7 +69,12 @@ public static <T> Serde<T> of(Class<T> clazz) {
5469

5570
/** Serialize/Deserialize class using the provided object mapper. */
5671
public static <T> Serde<T> of(ObjectMapper mapper, Class<T> clazz) {
57-
return new Serde<>() {
72+
return new RichSerde<>() {
73+
@Override
74+
public @Nullable Object jsonSchema() {
75+
return schemaGenerator.generateSchema(clazz);
76+
}
77+
5878
@Override
5979
public byte[] serialize(T value) {
6080
try {
@@ -89,7 +109,12 @@ public static <T> Serde<T> of(TypeReference<T> typeReference) {
89109

90110
/** Serialize/Deserialize {@link TypeReference} using the default object mapper. */
91111
public static <T> Serde<T> of(ObjectMapper mapper, TypeReference<T> typeReference) {
92-
return new Serde<>() {
112+
return new RichSerde<>() {
113+
@Override
114+
public @Nullable Object jsonSchema() {
115+
return schemaGenerator.generateSchema(typeReference.getType());
116+
}
117+
93118
@Override
94119
public byte[] serialize(T value) {
95120
try {

settings.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ dependencyResolutionManagement {
8181
.withoutVersion()
8282
library("jackson-jdk8", "com.fasterxml.jackson.datatype", "jackson-datatype-jdk8")
8383
.withoutVersion()
84+
library(
85+
"jackson-parameter-names",
86+
"com.fasterxml.jackson.module",
87+
"jackson-module-parameter-names")
88+
.withoutVersion()
8489
}
8590
create("kotlinLibs") {
8691
library("kotlinx-coroutines", "org.jetbrains.kotlinx", "kotlinx-coroutines-core")

0 commit comments

Comments
 (0)