Skip to content

Commit 100caee

Browse files
tvahrstsam0r040timonback
authored
Support for openapi v3/v3.1 schema formats #1291 (#1494)
* feat: schemaformat option for payload schemas added * feat: tests for schemaformat option for payload schemas added * chore: spotless * chore: javadoc * chore: spotless * fix: tests * fix: tests * chore(core): move wrapper logic for MultiFormatSchema to MultiFormatSchema, change back public api method from registerSimpleSchema to registerSchema to prevent breaking change, rename SwaggerSchemaService.postProcessSimpleSchema to postProcessSchemaWithoutRef to clarify the intention of the method Co-authored-by: Timon Back <[email protected]> * chore: ModelConvertersProvider and some minor enhancements. * chore: spotless * update tests for new ModelConvertersProvider bean. Moved initialization of ModelConvertersProvider from 'afterPropertiesSet' to the constructor. * feat(core): use PayloadSchemaFormat internally, switch to SchemaFormat only for the asyncapi artifact * chore: move OpenAPIv3 integration test to kafka example Kafka example includes many more listener, schemas and possible edge cases * spotless * refactor(core): remove PayloadSchemaFormat from API methods and use it directly from global configuration Co-authored-by: Timon Back <[email protected]> * chore(core): fix spotless Co-authored-by: sam0r040 <[email protected]> --------- Co-authored-by: David Müller <[email protected]> Co-authored-by: Timon Back <[email protected]> Co-authored-by: sam0r040 <[email protected]>
1 parent 020adff commit 100caee

File tree

38 files changed

+3727
-788
lines changed

38 files changed

+3727
-788
lines changed

springwolf-add-ons/springwolf-json-schema/src/test/java/io/github/springwolf/addons/json_schema/JsonSchemaGeneratorTest.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject;
1111
import io.github.springwolf.asyncapi.v3.model.schema.SchemaReference;
1212
import io.github.springwolf.asyncapi.v3.model.schema.SchemaType;
13-
import io.github.springwolf.core.asyncapi.schemas.SwaggerSchemaUtil;
13+
import io.github.springwolf.core.asyncapi.schemas.SwaggerSchemaMapper;
14+
import io.github.springwolf.core.configuration.properties.SpringwolfConfigProperties;
1415
import io.swagger.v3.core.util.Json;
1516
import io.swagger.v3.oas.models.media.ArraySchema;
1617
import io.swagger.v3.oas.models.media.BooleanSchema;
@@ -35,14 +36,14 @@
3536

3637
class JsonSchemaGeneratorTest {
3738
private final ObjectMapper mapper = Json.mapper();
38-
private final SwaggerSchemaUtil swaggerSchemaUtil = new SwaggerSchemaUtil();
39+
private final SwaggerSchemaMapper swaggerSchemaMapper = new SwaggerSchemaMapper(new SpringwolfConfigProperties());
3940
private final JsonSchemaGenerator jsonSchemaGenerator = new JsonSchemaGenerator();
4041

4142
@ParameterizedTest
4243
@MethodSource
4344
void validateJsonSchemaTest(String expectedJsonSchema, Supplier<Schema<?>> asyncApiSchema) throws Exception {
4445
// given
45-
SchemaObject actualSchema = swaggerSchemaUtil.mapSchema(asyncApiSchema.get());
46+
ComponentSchema actualSchema = swaggerSchemaMapper.mapSchema(asyncApiSchema.get());
4647

4748
// when
4849
verifyValidJsonSchema(expectedJsonSchema);
@@ -67,7 +68,7 @@ void validateJsonSchemaTest(String expectedJsonSchema, Supplier<Schema<?>> async
6768
ComponentSchema.of(pongSchema));
6869

6970
// when
70-
Object jsonSchema = jsonSchemaGenerator.fromSchema(ComponentSchema.of(actualSchema), definitions);
71+
Object jsonSchema = jsonSchemaGenerator.fromSchema(actualSchema, definitions);
7172

7273
// then
7374
String jsonSchemaString = mapper.writeValueAsString(jsonSchema);

springwolf-asyncapi/src/main/java/io/github/springwolf/asyncapi/v3/model/schema/MultiFormatSchema.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,11 @@ public class MultiFormatSchema extends ExtendableObject {
4949
*/
5050
@JsonProperty(value = "schema")
5151
private Object schema;
52+
53+
public static MultiFormatSchema of(Object schema) {
54+
// if payloadSchema.payload is already an instance of MultiFormatSchema, do not wrap again.
55+
return (schema instanceof MultiFormatSchema mfs)
56+
? mfs
57+
: MultiFormatSchema.builder().schema(schema).build();
58+
}
5259
}

springwolf-asyncapi/src/main/java/io/github/springwolf/asyncapi/v3/model/schema/SchemaFormat.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public enum SchemaFormat {
1212
ASYNCAPI_V3_JSON("application/vnd.aai.asyncapi+json;version=" + AsyncAPI.ASYNCAPI_DEFAULT_VERSION),
1313
ASYNCAPI_V3_YAML("application/vnd.aai.asyncapi+yaml;version=" + AsyncAPI.ASYNCAPI_DEFAULT_VERSION),
1414
OPENAPI_V3("application/vnd.oai.openapi;version=3.0.0"),
15+
OPENAPI_V3_1("application/vnd.oai.openapi;version=3.1.0"),
1516
OPENAPI_V3_JSON("application/vnd.oai.openapi+json;version=3.0.0"),
1617
OPENAPI_V3_YAML("application/vnd.oai.openapi+yaml;version=3.0.0"),
1718
JSON_SCHEMA_JSON("application/schema+json;version=draft-07"),

springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/ComponentsService.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.github.springwolf.asyncapi.v3.model.channel.message.MessageReference;
77
import io.github.springwolf.asyncapi.v3.model.components.ComponentSchema;
88
import io.github.springwolf.asyncapi.v3.model.schema.SchemaObject;
9+
import io.github.springwolf.asyncapi.v3.model.schema.SchemaReference;
910
import io.github.springwolf.core.configuration.properties.SpringwolfConfigProperties;
1011
import jakarta.annotation.Nullable;
1112

@@ -33,17 +34,17 @@ public interface ComponentsService {
3334
*
3435
* @param type Type to resolve a schema from
3536
* @param contentType Runtime ContentType of Schema
36-
* @return the root schema for the given type.
37+
* @return a {@link SchemaReference} referencing the root schema, or null if no schema could be resolved.
3738
*/
3839
@Nullable
3940
ComponentSchema resolvePayloadSchema(Type type, String contentType);
4041

4142
/**
4243
* registers the given schema with this {@link ComponentsService}
43-
* @param headers the schema to register, typically a header schema
44+
* @param schemaWithoutRef the schema to register, typically a header schema
4445
* @return the title attribute of the given schema
4546
*/
46-
String registerSchema(SchemaObject headers);
47+
String registerSchema(SchemaObject schemaWithoutRef);
4748

4849
/**
4950
* Provides a map of all registered messages.

springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/DefaultComponentsService.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
public class DefaultComponentsService implements ComponentsService {
2727

2828
private final SwaggerSchemaService schemaService;
29-
private final SpringwolfConfigProperties springwolfConfigProperties;
3029

3130
/**
3231
* maps a schema name (key) to a detected corresponding {@link ComponentSchema}.
@@ -51,11 +50,10 @@ public Map<String, ComponentSchema> getSchemas() {
5150
*
5251
* @param type Type to resolve a schema from
5352
* @param contentType Runtime ContentType of Schema
54-
* @return the root schema for the given type.
53+
* @return the root schema for the given type
5554
*/
5655
@Override
5756
public ComponentSchema resolvePayloadSchema(Type type, String contentType) {
58-
5957
SwaggerSchemaService.ExtractedSchemas payload = schemaService.resolveSchema(type, contentType);
6058
payload.referencedSchemas().forEach(schemas::putIfAbsent);
6159
return payload.rootSchema();
@@ -65,21 +63,21 @@ public ComponentSchema resolvePayloadSchema(Type type, String contentType) {
6563
* registers the given schema with this {@link ComponentsService}.
6664
* <p>NOTE</p>
6765
* Use only with schemas with max. one level of properties. Providing {@link SchemaObject}s with deep
68-
* property hierarchy will result in an corrupted result.
66+
* property hierarchy will result in a corrupted result.
6967
* <br/>
70-
* A typical usecase for this method is registering of header schemas, which have typically a simple structure.
68+
* A typical usecase for this method is registering of header schemas, which have typically a simple structure.
7169
*
72-
* @param headers the schema to register, typically a header schema
70+
* @param schemaWithoutRef the schema to register, typically a header schema
7371
* @return the title attribute of the given schema
7472
*/
7573
@Override
76-
public String registerSchema(SchemaObject headers) {
77-
log.debug("Registering schema for {}", headers.getTitle());
74+
public String registerSchema(SchemaObject schemaWithoutRef) {
75+
log.debug("Registering schema for {}", schemaWithoutRef.getTitle());
7876

79-
SchemaObject headerSchema = schemaService.extractSchema(headers);
80-
this.schemas.putIfAbsent(headers.getTitle(), ComponentSchema.of(headerSchema));
77+
ComponentSchema processedSchema = schemaService.postProcessSchemaWithoutRef(schemaWithoutRef);
78+
this.schemas.putIfAbsent(schemaWithoutRef.getTitle(), processedSchema);
8179

82-
return headers.getTitle();
80+
return schemaWithoutRef.getTitle();
8381
}
8482

8583
/**

springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/DefaultSchemaWalker.java

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.swagger.v3.oas.models.media.StringSchema;
88
import lombok.RequiredArgsConstructor;
99
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.lang.Nullable;
1011
import org.springframework.util.CollectionUtils;
1112

1213
import java.text.SimpleDateFormat;
@@ -175,7 +176,14 @@ private Optional<T> buildExampleFromUnvisitedSchema(
175176
return composedSchemaExample;
176177
}
177178

178-
String type = schema.getType();
179+
// schema may be an openapi v3 or v3.1 schema. While v3 uses an simple 'type' field, v3.1 supports a set of
180+
// types, for example ["string", "null"].
181+
182+
String type = getTypeForExampleValue(schema);
183+
if (type == null) {
184+
return Optional.empty();
185+
}
186+
179187
return switch (type) {
180188
case "array" -> buildArrayExample(schema, definitions, visited);
181189
case "boolean" -> exampleValueGenerator.createBooleanExample(DEFAULT_BOOLEAN_EXAMPLE, schema);
@@ -235,6 +243,33 @@ private String getFirstEnumValue(Schema schema) {
235243
return null;
236244
}
237245

246+
/**
247+
* looks in schemas openapi-v3 'type' and openapi-v3.1 'types' fields to
248+
* find the best candidate to use as an example value.
249+
*
250+
* @param schema
251+
* @return the type to use for example values, or null if no suitable type was found.
252+
*/
253+
@Nullable
254+
String getTypeForExampleValue(Schema schema) {
255+
// if the single type field is present, it has precedence over the types field
256+
if (schema.getType() != null) {
257+
return schema.getType();
258+
}
259+
260+
Set<String> types = schema.getTypes();
261+
262+
if (types == null || types.isEmpty()) {
263+
return null;
264+
}
265+
266+
return types.stream()
267+
.filter(t -> !"null".equals(t))
268+
.sorted() // sort types to be deterministic
269+
.findFirst()
270+
.orElse(null);
271+
}
272+
238273
private Optional<T> buildFromComposedSchema(
239274
Optional<String> name, Schema schema, Map<String, Schema> definitions, Set<Schema> visited) {
240275
final List<Schema> schemasAllOf = schema.getAllOf();

springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/grouping/GroupingService.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ private void markSchemas(AsyncAPI fullAsyncApi, MarkingContext markingContext, S
203203
* properties, allOf, anyOf, oneOf, not- and items references. Trys to deduce the schema id from the ref path and
204204
* returns a Set of detected schema ids.
205205
*
206-
* @param markingContext the current {@link MarkingContext}
206+
* @param markingContext the current {@link MarkingContext}
207207
* @param componentSchema the {@link ComponentSchema} to analyze
208208
* @return Set of schema ids representing nested schema refs
209209
*/
@@ -217,14 +217,16 @@ private static Set<String> findUnmarkedNestedSchemas(
217217
if (componentSchema.getMultiFormatSchema() != null) {
218218
MultiFormatSchema multiFormatSchema = componentSchema.getMultiFormatSchema();
219219

220-
// Currently we support async_api and open_api format.
220+
// Currently we support async_api and open_api v3 and v3.1 format.
221221
// The concrete schemaformat mediatype can contain json/yaml postfix, so we check wether the begin of the
222222
// media type matches.
223-
if (multiFormatSchema.getSchemaFormat().startsWith(SchemaFormat.ASYNCAPI_V3.toString())
223+
String schemaFormat = multiFormatSchema.getSchemaFormat();
224+
if (schemaFormat.startsWith(SchemaFormat.ASYNCAPI_V3.toString())
224225
&& multiFormatSchema.getSchema() instanceof SchemaObject schemaObject) {
225226
return findUnmarkedNestedSchemasForAsyncAPISchema(markingContext, schemaObject);
226227
}
227-
if (multiFormatSchema.getSchemaFormat().startsWith(SchemaFormat.OPENAPI_V3.toString())
228+
if ((schemaFormat.startsWith(SchemaFormat.OPENAPI_V3.toString())
229+
|| schemaFormat.startsWith(SchemaFormat.OPENAPI_V3_1.toString()))
228230
&& multiFormatSchema.getSchema() instanceof Schema openapiSchema) {
229231
return findUnmarkedNestedSchemasForOpenAPISchema(markingContext, openapiSchema);
230232
}
@@ -240,7 +242,7 @@ private static Set<String> findUnmarkedNestedSchemas(
240242
* returns a Set of detected schema ids.
241243
*
242244
* @param markingContext the current {@link MarkingContext}
243-
* @param schema the {@link SchemaObject} to analyze
245+
* @param schema the {@link SchemaObject} to analyze
244246
* @return Set of schema ids representing nested schema refs
245247
*/
246248
private static Set<String> findUnmarkedNestedSchemasForAsyncAPISchema(
@@ -276,12 +278,17 @@ private static Set<String> findUnmarkedNestedSchemasForAsyncAPISchema(
276278
* returns a Set of detected schema ids.
277279
*
278280
* @param markingContext the current {@link MarkingContext}
279-
* @param openapiSchema the Swagger {@link Schema} to analyze
281+
* @param openapiSchema the Swagger {@link Schema} to analyze
280282
* @return Set of schema ids representing nested schema refs
281283
*/
282284
private static Set<String> findUnmarkedNestedSchemasForOpenAPISchema(
283285
MarkingContext markingContext, Schema<?> openapiSchema) {
284-
Stream<Schema> propertySchemas = openapiSchema.getProperties().values().stream();
286+
final Stream<Schema> propertySchemas;
287+
if (openapiSchema.getProperties() != null) {
288+
propertySchemas = openapiSchema.getProperties().values().stream();
289+
} else {
290+
propertySchemas = Stream.empty();
291+
}
285292

286293
Stream<Schema> referencedSchemas = Stream.of(
287294
openapiSchema.getAllOf(), openapiSchema.getAnyOf(), openapiSchema.getOneOf())

springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/headers/HeaderClassExtractor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ public SchemaObject extractHeader(Method method, PayloadSchemaObject payload) {
3434
Header headerAnnotation = argument.getAnnotation(Header.class);
3535
String headerName = getHeaderAnnotationName(headerAnnotation);
3636

37-
SwaggerSchemaService.ExtractedSchemas extractedSchema = schemaService.extractSchema(argument.getType());
37+
SwaggerSchemaService.ExtractedSchemas extractedSchema =
38+
schemaService.postProcessSimpleSchema(argument.getType());
3839
ComponentSchema rootComponentSchema = extractedSchema.rootSchema();
3940

4041
// to stay compatible with former versions.

springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/headers/HeaderSchemaObjectMerger.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ public static SchemaObject merge(SchemaObject initial, SchemaObject... schemas)
2424
SchemaObject.SchemaObjectBuilder headerSchemaBuilder =
2525
SchemaObject.builder().type(SchemaType.OBJECT);
2626

27-
String title = initial.getTitle();
2827
String description = initial.getDescription();
2928
Map<String, Object> headerProperties = new HashMap<>(initial.getProperties());
3029

springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/scanners/common/message/AsyncAnnotationMessageService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ public MessageObject buildMessage(AsyncOperation operationData, Method method) {
4242
Map<String, MessageBinding> messageBinding =
4343
AsyncAnnotationUtil.processMessageBindingFromAnnotation(method, messageBindingProcessors);
4444

45-
var messagePayload = MessagePayload.of(
46-
MultiFormatSchema.builder().schema(payloadSchema.payload()).build());
45+
MultiFormatSchema multiFormatSchema = MultiFormatSchema.of(payloadSchema.payload());
46+
MessagePayload messagePayload = MessagePayload.of(multiFormatSchema);
4747

4848
var builder = MessageObject.builder()
4949
.messageId(payloadSchema.name())

0 commit comments

Comments
 (0)