Skip to content

NPE in SwaggerSchemaMapper.assignType when a property schema has no type (composed schema) #1834

Description

@jyrgopukk

Describe the bug

SwaggerSchemaMapper.assignType calls Set.of(schemaType) when the swagger Schema has a null type. Set.of rejects null, so any property whose schema is a composed schema (allOf / oneOf / anyOf, which legitimately carry no type) aborts channel and operation scanning with an NPE.

The failure is caught and logged as a WARN, so the published AsyncAPI document silently loses every channel the scanner had not reached yet. In our service 9 of 15 channels disappeared, and the only signal was four WARN lines at startup — the message body is literally null, so there was nothing to identify the offending payload with.

springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/schemas/SwaggerSchemaMapper.java (unchanged on main at the time of writing):

203  private static void assignType(Schema swaggerSchema, SchemaObject.SchemaObjectBuilder builder, boolean isNullable) {
204      Set<String> schemaTypes = swaggerSchema.getTypes();
205      Set<String> types = schemaTypes == null ? new HashSet<>() : new HashSet<>(schemaTypes);
206
207      String schemaType = swaggerSchema.getType();
208      if (!types.contains(schemaType)) {
209          // required while swagger v2 does not populate types
210          builder.type(Set.of(schemaType));   // NPE when schemaType == null
211          return;
212      }

Lines 205 and 208 already tolerate nulls (HashSet.contains(null) returns false), so a type-less schema is routed straight into the one call that cannot accept null.

How we ran into it

springdoc-openapi 3.1.0 added KotlinNullablePropertyCustomizer, which implements io.swagger.v3.core.converter.ModelConverter and is registered as a bean. ModelConvertersProvider:40 adds every ModelConverter bean in the Spring context to springwolf's converters, so it gets applied to Kafka payload schemas too.

For a nullable Kotlin property of object type it replaces the property schema with a composed wrapper (allOf: [$ref] + nullable under OAS 3.0, oneOf: [$ref, {type: "null"}] under 3.1). That wrapper has neither type nor $ref, which is valid OpenAPI, and assignType then throws.

springdoc is just how we hit it — any converter emitting a type-less composed schema would do the same.

Suggested fix

builder.type(schemaType == null ? Set.of() : Set.of(schemaType));

Separately, it would help a lot if DefaultChannelsService / DefaultOperationsService logged the schema or class being scanned when they catch an exception. The current WARN carries only null, which meant tracking this down required decompiling to find out which payload failed.

Dependencies and versions used

  • springwolf-core version 2.6.0 (code path unchanged on main)
  • springwolf-kafka version 2.6.0
  • springdoc-openapi-starter-webflux-api version 3.1.0
  • Spring Boot 4.1.0, Kotlin 2.3.10, JVM 25

Code example

Payload (any nullable, object-typed property will do):

data class AuthorisationPayload(
    val serviceReference: String,
    val merchant: Merchant?,   // nullable + object type -> composed schema with no type
) {
    data class Merchant(val settingsId: String)
}

Reproducer without booting Spring — this throws:

val properties = SpringwolfConfigProperties()
val provider = ModelConvertersProvider(
    properties,
    listOf(KotlinNullablePropertyCustomizer(ObjectMapperProvider(SpringDocConfigProperties()))),
)
val service = SwaggerSchemaService(properties, emptyList(), SwaggerSchemaMapper(properties), provider)

service.resolveSchema(AuthorisationPayload::class.java, "application/json")  // NullPointerException

In a running application the same thing happens during startup scanning, with the customizer picked up automatically as a bean. Removing that single bean (springdoc.model-converters.kotlin-nullable-property-customizer.enabled: false) restores all 15 channels, which is our current workaround.

Stack trace and error logs

WARN i.g.s.c.a.c.DefaultChannelsService   : An error was encountered during channel scanning with
                                            ChannelsInClassScannerAdapter@2ada96fd: null
WARN i.g.s.c.a.o.DefaultOperationsService : An error was encountered during operation scanning with
                                            OperationsInClassScannerAdapter@41820105: null

java.lang.NullPointerException
    at java.base/java.util.Objects.requireNonNull(Objects.java:220)
    at java.base/java.util.ImmutableCollections$Set12.<init>(ImmutableCollections.java:1009)
    at java.base/java.util.Set.of(Set.java:470)
    at io.github.springwolf.core.asyncapi.schemas.SwaggerSchemaMapper.assignType(SwaggerSchemaMapper.java:210)
    at io.github.springwolf.core.asyncapi.schemas.SwaggerSchemaMapper.mapSwaggerSchemaToAsyncApiSchema(SwaggerSchemaMapper.java:100)
    at io.github.springwolf.core.asyncapi.schemas.SwaggerSchemaMapper.mapSchema(SwaggerSchemaMapper.java:72)
    at io.github.springwolf.core.asyncapi.schemas.SwaggerSchemaMapper.mapSchemaOrRef(SwaggerSchemaMapper.java:51)
    at io.github.springwolf.core.asyncapi.schemas.SwaggerSchemaMapper.lambda$mapSwaggerSchemaToAsyncApiSchema$1(SwaggerSchemaMapper.java:105)
    at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:214)
    at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
    ...

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions