Repository navigation
Clarify or revise OpenApiSchemaReference semantics for $ref sibling keywords #2919
Description
Problem
OpenAPI.NET currently preserves OpenAPI 3.1/3.2 $ref schema siblings on JsonSchemaReference and exposes them through OpenApiSchemaReference convenience getters using reference-first fallback behavior:
Reference.X ?? Target?.XThis behavior appears to be intentional. In #2903, Vincent Biret (@baywet) wrote after discussions with Henry Andrews (@handrews) and Darrel (@darrelmiller) that:
Any JSON Schema keyword can appear next to
$ref... some might be contradictory when appearing both in the reference and the referenced schema, in which case, it's up to the application to define what the behaviour should be.
And:
In the case where both are present, we have a precedent of returning the reference value for annotation keywords, so being consistent here makes sense. Then the application can check the reference and the target values, and compare them, if the difference is important to the application.
That explains why #2906 made the schema keyword getters reference-first across all keyword types.
The concern is that this convenience behavior can be easy to misread for JSON Schema assertion keywords.
In JSON Schema 2020-12, $ref siblings are evaluated additively. They are not overrides of the referenced schema.
Example:
components:
schemas:
Target:
type: object
minProperties: 5
Referencing:
$ref: '#/components/schemas/Target'
minProperties: 2Both constraints apply. The sibling minProperties: 2 does not relax the target's minProperties: 5.
With the current object model:
schemaRef.MinProperties; // 2
schemaRef.Reference.MinProperties; // 2
schemaRef.Target.MinProperties; // 5This can make assertion keywords appear to override target constraints, even though JSON Schema evaluation treats them compositionally.
Question
Should OpenApiSchemaReference getters continue to expose reference-first convenience behavior for all schema keywords, or should the library distinguish more explicitly between:
- authored sibling keywords on
JsonSchemaReference - resolved target schema values via
Target - an evaluation-oriented/composed schema view
Possible Directions
Option 1: Keep current behavior and document it
Keep Reference.X ?? Target?.X for all schema keyword getters.
Document that these getters are object-model convenience accessors, not a JSON Schema evaluation/effective-schema view. Consumers that care about conflicts or additive constraints should inspect both schemaRef.Reference and schemaRef.Target.
Option 2: Split annotation and assertion getter behavior
Keep reference-first getters for annotation-style keywords such as:
DescriptionTitleDefaultDeprecatedReadOnlyWriteOnlyExamplesExtensions
Return target values for assertion/structural keywords such as:
TypeFormatMinPropertiesMaxPropertiesPatternRequiredPropertiesAllOfOneOfAnyOfNotItemsIfThenElse
This is more validation-oriented, but it breaks the uniform getter rule from #2903 and creates setter/getter asymmetry. For example, setting schemaRef.Type stores the sibling value on Reference, but reading schemaRef.Type would return the target value.
Option 3: Keep current getters and add an explicit evaluation/composition API later
Keep the current object-model behavior for compatibility and consistency.
Add a future API such as GetEffectiveSchema() or similar for consumers that need JSON Schema evaluation semantics. That API could preserve additive sibling assertions compositionally, for example by representing inlined references with allOf rather than flattening sibling assertions as overrides.
Related Concern: Inline Serialization
CopyReferenceAsTargetElementWithOverrides() currently creates an inline OpenApiSchema from the reference view. Because the reference view is reference-first, inline serialization can flatten target-plus-sibling state in a way that looks like sibling assertions override target assertions.
A semantically safer inline representation would preserve composition, for example:
allOf:
- <inlined target schema>
minProperties: 2This may be better handled separately from the getter behavior.
Related
- feat(schema): resolve bare $dynamicRef via $dynamicAnchor index #2913 extends the existing sibling-preservation model to bare
$dynamicRefschemas. - Feature: Resolve $dynamicRef against $dynamicAnchor in the schema reference resolution pipeline #2911 tracks
$dynamicRef/$dynamicAnchorresolution.
Activity
aqeelat commentedon Jul 1, 2026
I dug into the recent history here and I think this needs maintainer direction before changing the getters.
The current
Reference.X ?? Target?.Xbehavior appears to be intentional, not accidental. In #2903, Vincent Biret (@baywet) wrote after discussions with Henry Andrews (@handrews) and Darrel (@darrelmiller) that:And:
That explains why #2906 made the schema keyword getters reference-first.
The concern in this issue still seems valid: for assertion/structural keywords, reference-first getters can look like sibling keywords override target constraints, while JSON Schema 2020-12 evaluates them additively. A sibling
minProperties: 2does not relax a targetminProperties: 5.I see three possible directions (updated the issue body with details):
GetEffectiveSchema()) so consumers can opt into JSON Schema evaluation semantics without changing the current object-model behavior.My leaning is that option 1 or 3 may be safer than changing the existing getters immediately, because the reference-first behavior was an explicit recent design decision rather than an implementation accident.
handrews commentedon Jul 2, 2026
This is not quite correct, which I missed when Vincent Biret (@baywet) stated it. It is true for annotation keywords that the application decides, but not for assertions (or applicators like
properties). Abdullah Alaqeel (@aqeelat) is correct regarding assertions: They MUST be evaluated independently.handrews commentedon Jul 2, 2026
It's also worth noting that not all annotation should have the same behavior. For something like
title, an application might define a particular precedence. But for something likereadOnly... if anything marks an instance locationreadOnly: truethen it SHOULD (per the JSON Schema specification) be considered read-only even if some other subschema marks itreadOnly: false.There is no universal rule whereby
$ref-adjacent and$ref-target keywords can be collapsed. For some annotations, it makes sense to defer to the application. But for others, there are fairly clear sensible vs counter-intuitive behaviors.Plus, some keywords such as
minContainsandmaxContainstechnically function as "annotations" in the sense that they provide information tocontainsrather than providing their own assertion result (the exact wording here depends on the draft- we didn't get it right the first time). This is not really ideal, and this use of annotations as a communication channel is something we're trying to sort out with a better solution in the latest IETF draft (which still uses the 2020-12 metaschema).baywet commentedon Jul 2, 2026
Thanks for chiming in Henry Andrews (@handrews)! It's really appreciated!
Considering the "correct" behaviour is keyword specific between:
And given the library users can access all values (reference defined, target defined) only recently (from most keywords, they only had access to the target values). I suggest that we... maintain a consistent behaviour (reference value is returned when both are present), punt the decision and document that aspect explicitly in the OpenApiSchemaReference class.
Here is the reasoning, those proxy getters are convenience, and starting with consistency abides by the "least surprise principle". Applications that need to be "really precise", can go the extra mile and compare values for the keywords with the current API.
Thoughts?
handrews commentedon Jul 2, 2026
Vincent Biret (@baywet) References never inherently take precedence. Actually, nothing inherently takes precedence over anythingn else. The keywords that do interact across subschema boundaries, like
unevaluatedProperties, have very specific mechanisms for doing so and do not operate by simple precedence rules.The thing about annotations being different is that when you actually evaluate an instance, all of the annotations from schemas that validated successfully (e.g. not from the failed branches of a
oneOf, and never from undernotas either the subschema failed (so no annotations) or it passed and thennotcaused it to fail (so the annotations are ignored)) are attached to the instance. So the whole "the application can decide what to do with them" is predicated on the idea that the application gets all of the annotation values, including how they were applied (e.g. was this from a subschema with a$refor was it reached via a$ref, etc.), and that information can be used to determine the behavior. So then if there are twotitleannotations on a single instance location, and one was adjance to the$refand one was reached through the$ref, the application can then decide what to do with that.Some of that runtime behavior can be analyzed statically, although if you have a lot of conditional keywords like
oneOfthen at some point things get very complex and you either just need to run schema validation or you're de-facto simulating it in some weird way.handrews commentedon Jul 2, 2026
Only in the 3.1+ Reference Object (which is not used for schemas) do adjacent fields override anything. The Path Item Object has still different behavior with
$refand adjacent fields, which is very vaguely defined, and boils down to "if they don't conflict, it's fine, and if they do, all bets are off, good luck."baywet commentedon Jul 2, 2026
Thank you for the additional information.
I believe the focus of this discussion is JSON Schema references only. For any other kind of reference, this library simply resolves them (and exposes peer keywords like summary or description, but that's about it).
Another distinction I believe I never shared with you specifically is that this library does not do any validation. It one parses/serializes/offers an object model for OpenAPI. From that object model, it's up to the application to implement the validation aspects (or use another library).
I hope that clarifies the context here?
aqeelat commentedon Jul 2, 2026
Thanks, both. The extra context helps.
One thing I should have included in the original issue: this came up while working on MicrosoftDocs/openapi-docs#168, which tries to document OpenAPI.NET behavior for OpenAPI 3.1
$refschema siblings.That docs PR needs to explain several things at once:
JsonSchemaReferenceOpenApiSchemaReference.ReferenceOpenApiSchemaReference.TargetOpenApiSchemaReferenceconvenience getters currently prefer the reference value when presentThe hard part is that the object-model convenience behavior and JSON Schema evaluation behavior are easy to conflate.
Given the discussion here, I think the safest direction is:
OpenApiSchemaReference.ReferenceandTarget, or use a JSON Schema validator / future composed-schema API.properties,contains+minContains,additionalProperties,unevaluatedProperties, raw numeric precision, and mutable collection semantics.I opened #2921 before this discussion fully converged. Based on the latest comments, I think that PR should probably be reduced to documenting the getter semantics on
OpenApiSchemaReferencerather than changing behavior. That would also give MicrosoftDocs/openapi-docs#168 a clear contract to document.docs(schema): document OpenApiSchemaReference getter semantics