Skip to content

feat: ModelCopy.deepCopy, an exact deep copy of OpenAPI models - #5364

Merged
daniel-kmiecik merged 2 commits into
swagger-api:3.0.0from
bnasslahsen:feat/model-deep-copy
Oct 27, 2026
Merged

daniel-kmiecik merged 2 commits into
swagger-api:3.0.0from
bnasslahsen:feat/model-deep-copy

Conversation

@bnasslahsen

Copy link
Copy Markdown

Pull Request

Description

Found while porting springdoc-openapi to Swagger Core 3.0 (feedback in #4991).

Frameworks need to copy models: for example, to customize a copy of the user's OpenAPI bean for each API group,
or to adapt a resolved Schema without changing the one in components. Today the only way is a JSON round trip,
which is what AnnotationsUtils.clone, SpecFilter and springdoc-openapi do. That isn't a copy. I measured the
round trip on the 3.0.0 branch with a model covering the usual cases:

Kept after the copy Json.mapper() round trip Json31.mapper() round trip generic mapper round trip Java serialization¹ ModelCopy.deepCopy
equals(source) ❌ ❌ ❌ ✅ ✅
concrete classes (StringSchema, ArraySchema) ✅ (but Schema → ObjectSchema) ❌ all JsonSchema ❌ all Schema ✅ ✅
Schema.getName() ❌ ❌ ❌ ✅ ✅
explicit example(null) ✅ ✅ ❌ ✅ ✅
3.1 fields of a 3.1 model (types, $id) ❌ ✅ ✅ ✅ ✅
BigDecimal example 1.50 1.5 Double 1.5 Double 1.5 ✅ ✅
LocalDate / JsonNode / user object example String / Map / Map same same ✅ ✅
null extension values ❌ ❌ ❌ ✅ ✅
time per copy (rough) ~50–140 µs ~70 µs ~2.8 ms ~120–170 µs ~90 µs

¹ After making the 27 base model classes Serializable on a scratch branch. It fails with
NotSerializableException as soon as an example, default, enum or extension value isn't Serializable, and it would
add a serialization contract to every model, so it isn't proposed. An annotation-free Jackson mapper with default
typing was also tested. It keeps most of the above, but fails on user values without a no-arg constructor, turns
Optional into its content, and normalizes OffsetDateTime to UTC.

ModelCopy.deepCopy(T) copies the graph structurally:

  • models (including subclasses defined outside io.swagger.v3.oas.models), lists, sets and maps are copied;
  • JsonNodes are deep-copied;
  • other values (strings, numbers, dates, enums, user objects) are shared and treated as immutable;
  • shared references within the graph are kept.

It's one self-contained class, with no changes to the models.

AnnotationsUtils.clone is intentionally unchanged. I tried delegating it: 5 tests fail, because ModelResolver
relies on the round trip's normalization, for example to JsonSchema with OpenAPI 3.1. Swagger Core can adopt
ModelCopy internally later, case by case.

Fixes:

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • ♻️ Refactor (non-breaking change)
  • 🧪 Tests
  • 📝 Documentation
  • 🧹 Chore (build or tooling)

Checklist

  • I have added/updated tests as needed: ModelCopyTest
  • I have added/updated documentation where applicable: Javadoc
  • The PR title is descriptive
  • The code builds and passes tests locally: swagger-core and swagger-rest, on Jackson 3.2.3
  • I have linked related issues (if any): [Feature] Support Jackson 3.x release line #4991

Copying models through a JSON round trip (what AnnotationsUtils.clone,
SpecFilter and downstream projects do) isn't a copy: depending on the
mapper it loses Schema.getName(), the "example set" flag, the spec
version, the concrete model classes (StringSchema becomes Schema,
ObjectSchema or JsonSchema), the fields of the other OpenAPI version,
null extension values, and the type of example/default values
(BigDecimal 1.50 becomes 1.5 or a Double, LocalDate a String, a JsonNode
or user object a LinkedHashMap).

ModelCopy.deepCopy copies the model graph structurally: models
(including subclasses), lists, sets and maps are copied, JsonNodes are
deep-copied, other values are shared, and shared references are kept.

AnnotationsUtils.clone is unchanged: ModelResolver relies on its
normalization.

See swagger-api#4991.

Signed-off-by: Badr NASS LAHSEN <badr.nasslahsen@gmail.com>
@daniel-kmiecik
daniel-kmiecik merged commit a9e1fae into swagger-api:3.0.0 Oct 27, 2026
8 checks passed
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.

2 participants