Problem
When two allOf branches redeclare a property and one of them writes an unknown format, the merged property depends on which branch comes first. The property is typed by one declaration, the other is parked, and nothing is reported.
openapi: 3.1.0
info: {title: t, version: "1"}
paths: {}
components:
schemas:
Holder:
allOf:
- {type: object, properties: {token: {type: string}}}
- {type: object, properties: {token: {type: string, format: hex-token, minLength: 8, default: x, description: the token}}}
| Branch order |
token type |
default |
minLength |
Diagnostics |
| plain first (above) |
t/prim/string |
absent |
absent |
none |
hex-token first |
the hoisted t/anon/.../allOf/0/properties/token |
x |
8 |
none |
In both orders the losing declaration is kept verbatim under openapi:conflicting-redeclaration/<pointer> on the property, so no keyword leaves the document. The typed fields differ, though. With the plain branch first, the property has no default, no constraints and no format, and the format-bearing declaration survives only as an unreferenced node plus that Unmodeled entry. Reordering two branches changes the merged field.
format: password joins this family once #592 lands, because it stops resolving to the shared string primitive. A format that hoists over a different base, such as byte, is not silent: it reports an incompatible-types warning, and it also keeps whichever declaration comes first.
Cause
Merger.recordRedeclarationConflict (compilers/openapi/internal/merge/merge.go) treats two declarations with different Type.Target as one dropping the other. typesConflict resolves each side to its primitive kind and finds both string, so it reports nothing. The hoisted Scalar and the shared primitive are different targets of the same kind.
This is a different mechanism from #446 and #598. Those are about typesConflict reporting a conflict between primitive kinds that can both hold. Here no conflict is reported, and the shape fold still discards one declaration.
Expected
A format that hoists a node over the same base kind narrows the bare type, as uri narrows string. The narrower declaration should win the shape whatever the branch order, and the other declaration's details (default, constraints, description, examples) should fold in as they do for a redeclaration that does not hoist. TestAllOf_ReconcileAccumulatesRicherDetailWhateverTheOrder already states this for branch order.
Acceptance
Problem
When two
allOfbranches redeclare a property and one of them writes an unknownformat, the merged property depends on which branch comes first. The property is typed by one declaration, the other is parked, and nothing is reported.tokentypedefaultminLengtht/prim/stringhex-tokenfirstt/anon/.../allOf/0/properties/tokenx8In both orders the losing declaration is kept verbatim under
openapi:conflicting-redeclaration/<pointer>on the property, so no keyword leaves the document. The typed fields differ, though. With the plain branch first, the property has nodefault, no constraints and no format, and the format-bearing declaration survives only as an unreferenced node plus thatUnmodeledentry. Reordering two branches changes the merged field.format: passwordjoins this family once #592 lands, because it stops resolving to the shared string primitive. A format that hoists over a different base, such asbyte, is not silent: it reports an incompatible-types warning, and it also keeps whichever declaration comes first.Cause
Merger.recordRedeclarationConflict(compilers/openapi/internal/merge/merge.go) treats two declarations with differentType.Targetas one dropping the other.typesConflictresolves each side to its primitive kind and finds bothstring, so it reports nothing. The hoistedScalarand the shared primitive are different targets of the same kind.This is a different mechanism from #446 and #598. Those are about
typesConflictreporting a conflict between primitive kinds that can both hold. Here no conflict is reported, and the shape fold still discards one declaration.Expected
A format that hoists a node over the same base kind narrows the bare type, as
urinarrowsstring. The narrower declaration should win the shape whatever the branch order, and the other declaration's details (default, constraints, description, examples) should fold in as they do for a redeclaration that does not hoist.TestAllOf_ReconcileAccumulatesRicherDetailWhateverTheOrderalready states this for branch order.Acceptance
compose_test.gofixture that fix(compilers/openapi): keep format: password at every position #592 narrowed to avoid the format goes back to writing one, andTestAllOf_RedeclarationOrsSecretasserts the folded fields as well asSecret.