Skip to content

Conversation

@tvahrst
Copy link
Contributor

@tvahrst tvahrst commented Nov 2, 2025

I introduced a new Property springwolf.docket.payload-schema-format which is an enum and supports currently

  • asyncapi_v3 (the default)
  • openapi_v3
  • openapi_v3_1

With the default-setting (asyncapi_v3), SpringWolf behaves as before.

Enabling openapi_v3 or _v3_1 leads to a AsyncApi where the payload schemas (not the header schemas!) are formatted with openapi v3/v3.1 format. This is possible and allowed because the AsyncApi Spec supports the definition of alternative schema formats.

The openapi v3/v3.1 schema formats are especially helpful when it comes to polymorphic schemas with custom discrimiantor mappings (which are not possible to describe with default asyncapi schema format).

@netlify
Copy link

netlify bot commented Nov 2, 2025

Deploy Preview for springwolf-ui canceled.

Name Link
🔨 Latest commit f3b6093
🔍 Latest deploy log https://app.netlify.com/projects/springwolf-ui/deploys/690e2b73c88c790008bea87b

}

String type = schema.getType();
// schema may be an openapi v3 or v3.1 schema. While v3 uses an simple 'type' field, v3.1 supports a set of
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

building examples from types happend to be a little bit 'tricky' because openapi v3 and v3.1 changed the field for the type(s). I moved the "type-lookup" in an extra method 'getTypeForExampleValue'.

public ExtractedSchemas resolveSchema(Type type, String contentType, SchemaFormat schemaFormat) {
String actualContentType =
StringUtils.isBlank(contentType) ? properties.getDocket().getDefaultContentType() : contentType;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is one of the main implementation point: Decision, which ModelConverts to use. AsyncApi is currently based on openapi-V3

* @return the resulting AsnycApi SchemaObject
*/
public SchemaObject mapSchema(Schema value) {
public ComponentSchema mapSchema(Schema swaggerSchema, SchemaFormat schemaFormat) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the point where the generated Swagger-schema (from swagger ModelConverters) is mapped to the target schema format. For openapi v3/v3.1, no transformation is necessary, only wrapping the schema into an MultiFormatschema.

For asyncapi schemaformat (the default), the existing mapping logic is used (mapSwaggerSchemaToAsyncApiSchema

@JsonSubTypes.Type(value = Dog.class, name = "dog"),
@JsonSubTypes.Type(value = Cat.class, name = "cat"),
})
@Schema(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extended the Pet Superclass with @Schema Annotation and discriminator mapping Infos. These Infos are ignored for asyncapi schemaformat (because asyncapi does not support custom mappings) - but are detected and used for openapi schema formats.

@sam0r040 sam0r040 force-pushed the componentschema-refactoring branch from d8d540e to ad80cfa Compare November 7, 2025 15:35
Copy link
Member

@timonback timonback left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sam0r040 and I reviewed the PR. We enjoyed the PR very much. The PR is surprisingly sleek and it shows that you have understood and found the best place for integrating this feature.

We added some comments, suggestions. If there are questions, let us know. Some comments probably also reflect the Springwolf code style.
FYI: We committed & pushed a couple smaller changes (i.e. avoiding breaking API change)

Schema rootSchema, Map<String, Schema> referencedSchemas, SchemaFormat schemaFormat) {
ComponentSchema rootComponentSchema = swaggerSchemaUtil.mapSchemaOrRef(rootSchema, schemaFormat);
Map<String, ComponentSchema> referencedComponentSchemas =
swaggerSchemaUtil.mapSchemasMap(referencedSchemas, schemaFormat);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch, that the empty hashmap is not used

PayloadSchemaFormat payloadSchemaFormat =
springwolfConfigProperties.getDocket().getPayloadSchemaFormat();
this.payloadSchemaFormat = payloadSchemaFormat.getSchemaFormat();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personal opinion: prefer auto-generated constructor and thereby avoiding the substitution of configProperties

*/
public SchemaObject mapSchema(Schema value) {
public ComponentSchema mapSchema(Schema swaggerSchema, SchemaFormat schemaFormat) {
return switch (schemaFormat) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: the current mix up of openapi 3.0 and 3.1 schemas still needs to be addressed in the mapper (other open issue, out of scope for this pr)

void classWithSchemaAnnotationWithAsyncapiSchemaformat() {
ComponentSchema schema = schemaService
.resolveSchema(ClassWithSchemaAnnotation.class, "content-type-not-relevant")
.resolveSchema(ClassWithSchemaAnnotation.class, "content-type-not-relevant", SchemaFormat.ASYNCAPI_V3)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

review: does class need more test cases or is it fine to just add the method parameter?


import static org.assertj.core.api.Assertions.assertThat;

public class AsyncApiDocumentWithOpenApiV31SchemaFormatIntegrationTest {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

review: do we need all the tests on this level?
I wonder if one happy path test case is sufficient. Feels like a lot of test duplication

@@ -0,0 +1,44 @@
// SPDX-License-Identifier: Apache-2.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure whether the jms example is the best place for these tests, as it has not a lot of dtos.
So far, we have mostly used the kafka example.

Similiarly, we can use the new StandaloneApplication test, which executes faster because it does not need a full spring boot context. Environment variables (for the schema format) can be injected using the builder

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One second thought, we believe that we can skip this test.

The examples are particular show cases of the main functionality - not all types of configurations possible.
Second (out of scope), we can benefit from integration tests in core with the asyncapi.json file checked in (currently tested manual in the two classes that you duplicated).

Also, the actual mapping is done by swaggers ModelConverter (a library that we do not control). Therefore, we see it not as much in our test scope.

public class SwaggerSchemaUtil {

public Map<String, SchemaObject> mapSchemasMap(Map<String, Schema> schemaMap) {
public Map<String, ComponentSchema> mapSchemasMap(Map<String, Schema> schemaMap, SchemaFormat schemaFormat) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered using the schemaFormat as a global setting and then injecting the SpringwolfConfigProperties?

Over the time the SwaggerSchemaUtil has grown and we could imagine that the SpringwolfConfigProperties become a constructor argument when rename to i.e. SwaggerSchemaMapper. (It does not feel like an util anymore)

private final ModelConverters converter = ModelConverters.getInstance();
private final ModelConverters converter_openapi30 = ModelConverters.getInstance(false);
private final ModelConverters converter_openapi31 = ModelConverters.getInstance(true);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered moving these two fields into a ModelConvertersProvider?

We stumpled upon this, because the related tests verify that the correct model provider switch case is used, but not that the actual mapping work as expected. Still, actual mapping is probably best used in an integration test anyway.

/**
* Enumeration defining the supported payload schema formats, for use in SpringwolfConfigProperties.
*/
public enum PayloadSchemaFormat {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not sure if it a good idea to directly map the payload schema into the SchemaFormat enum, which has more types.
It couples the values of this enum to the other other and leaves less room for configuration changes.

Similarly, it feels odd in the switch expression to have an else branch, that can never be reached - at this moment in time, but is required technically for the compiler.

Suggestion:
ASYNCAPI_V3, OPENAPI_V3, OPENAPI_V3_1;

And the PayloadSchemaFormat is used in the code, including the switch expression

String url = "/springwolf/docs";
String actual = restTemplate.getForObject(url, String.class);
String actualPatched = actual.replace("localhost:61616", "activemq:61616");
Files.writeString(Path.of("src", "test", "resources", "asyncapi.actual.json"), actualPatched);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Files.writeString(Path.of("src", "test", "resources", "asyncapi.actual.json"), actualPatched);
Files.writeString(Path.of("src", "test", "resources", "asyncapi.openapiv31.actual.json"), actualPatched);

@sam0r040 sam0r040 force-pushed the componentschema-refactoring branch from 23e6862 to 98a085c Compare November 7, 2025 17:12
…chema, 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 <timonback@users.noreply.github.com>
@sam0r040 sam0r040 force-pushed the componentschema-refactoring branch from 98a085c to f3b6093 Compare November 7, 2025 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants