Skip to content

fix: prevent examples duplication in Schema model - #5330

Open
vpelikh wants to merge 1 commit into
swagger-api:3.0.0from
vpelikh:pr-20-prevent-examples-duplication
Open

vpelikh wants to merge 1 commit into
swagger-api:3.0.0from
vpelikh:pr-20-prevent-examples-duplication

Conversation

@vpelikh

@vpelikh vpelikh commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes duplicated examples entries on a schema when the same @Schema(examples = {...}) annotation is applied to a schema twice. The examples resolution is now idempotent.

Problem

ModelResolver.resolveSchemaMembers(...) merges the examples member of the @Schema annotation into any examples already present on the target schema:

if (schemaAnnotation.examples().length > 0) {
    List<Object> parsedExamples = AnnotationsUtils.parseExamplesArray(schemaAnnotation, schema);
    if (schema.getExamples() == null || schema.getExamples().isEmpty()) {
        schema.setExamples(parsedExamples);
    } else {
        schema.getExamples().addAll(parsedExamples);   // <-- not idempotent
    }
}

The resolver is not guaranteed to visit a given schema exactly once. When it processes the same annotation for a schema that already carries examples (for example when a downstream tool such as springdoc-openapi re-processes an already-resolved schema during the Jackson 3 migration), the addAll branch appends the parsed values again, producing output such as:

"examples": ["Hello", "World", "Hello", "World"]

instead of

"examples": ["Hello", "World"]

This was observed in practice after the Jackson 3 migration, where schema resolution is re-run and the previously merged examples were duplicated.

Change

Replace the conditional merge with a plain assignment so that the annotation is always authoritative for the examples of the schema it is applied to:

if (schemaAnnotation.examples().length > 0) {
    List<Object> parsedExamples = AnnotationsUtils.parseExamplesArray(schemaAnnotation, schema);
    schema.setExamples(parsedExamples);
}

parseExamplesArray(...) already reads the current schema state (it consults schema.type() and schema.nullable() to decide how to parse each value), so no information is lost by assigning rather than appending.

Tests

Added ModelResolverExamplesTest:

  • resolvingSchemaMembersTwiceDoesNotDuplicateExamples — applies the same @Schema(examples) to a schema that already carries examples and asserts the list is unchanged.
  • resolvingSameModelTwiceKeepsExamplesStable — resolves a model once, re-runs the schema member resolution, and asserts the examples list is unchanged.

Both tests fail before the fix (expected [2] but found [4]) and pass after it.

Alternatives considered

An alternative fix keeps the merge but de-duplicates:

for (Object e : parsedExamples) {
    if (!schema.getExamples().contains(e)) {
        schema.getExamples().add(e);
    }
}

This was rejected because AnnotationsUtils.mergeSchemaAnnotations(...) resolves the type-level and member-level examples() by picking one side (it never concatenates them), so by the time this code runs schema.getExamples() can only hold the current annotation's own values, from a previous visit. There is no distinct source to merge, so plain assignment is equivalent and simpler. Also verified empirically that the two variants produce identical output for member-only, type-level-only, and member-plus-type-level cases.

Notes

This is the upstream port of fork PR vpelikh#20.

ModelResolver.resolveSchemaMembers() merged the examples parsed from
@Schema(examples = {...}) into any examples already present on the schema:

    if (schema.getExamples() == null || schema.getExamples().isEmpty()) {
        schema.setExamples(parsedExamples);
    } else {
        schema.getExamples().addAll(parsedExamples);
    }

The resolver can apply the same annotation to the same schema more than
once (e.g. when a downstream tool such as springdoc-openapi re-processes an
already resolved schema). The addAll branch then appended the same values
again, producing duplicated entries such as
["Hello", "World", "Hello", "World"].

Assign instead of merge so the resolution is idempotent. This is safe
because AnnotationsUtils.mergeSchemaAnnotations() resolves the type-level and
member-level examples() by picking one side (it never concatenates them), so
there is no distinct source to merge.

Adds ModelResolverExamplesTest covering both the direct double-application
and the re-resolve path; both fail before the fix (expected [2] but
found [4]) and pass after it.

This branch has not been deployed

No deployments
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.

1 participant