Repository navigation
Clarify or revise OpenApiSchemaReference semantics for $ref sibling keywords #2919
Description
Activity
aqeelat commented
on Jul 1, 2026 ContributorAuthorMore actionsI 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: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.
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):
- Keep the current getter behavior and document it as an object-model convenience, not an evaluation/effective-schema view.
- Split annotation and assertion getter behavior — keep reference-first for annotations, return target values for structural/assertion keywords. More validation-oriented, but breaks the uniform getter rule from Add missing keywords support for OpenAPIReference (and JsonSchemaRefernece #2903 and creates setter/getter asymmetry.
- Keep existing getters and add a separate composed/evaluation-oriented API later (e.g.
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.
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.
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.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).Thanks for chiming in Henry Andrews (@handrews)! It's really appreciated!
Considering the "correct" behaviour is keyword specific between:
- reference takes precedence
- both need to be evaluated
- we can be smart and give the "most applicable value" (min max kind of situation)
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?
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.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."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 commented
on Jul 2, 2026 ContributorAuthorMore actionsThanks, 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:
- OpenAPI.NET preserves sibling keywords on
JsonSchemaReference - authored sibling values are available through
OpenApiSchemaReference.Reference - resolved target values are available through
OpenApiSchemaReference.Target OpenApiSchemaReferenceconvenience getters currently prefer the reference value when present- JSON Schema evaluation does not treat sibling assertions/applicators as replacements for the target schema
The 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:
- Keep the existing reference-first convenience getter behavior for
OpenApiSchemaReference. - Explicitly document that these getters are not an effective/evaluation-oriented JSON Schema view.
- Tell consumers that need precise JSON Schema semantics to inspect both
ReferenceandTarget, or use a JSON Schema validator / future composed-schema API. - Avoid partial keyword-specific "effective" behavior in these getters, because that quickly runs into cases like
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.- OpenAPI.NET preserves sibling keywords on
- added a commit that references this issue
on Jul 15, 2026
Problem
OpenAPI.NET currently preserves OpenAPI 3.1/3.2
$refschema siblings onJsonSchemaReferenceand exposes them throughOpenApiSchemaReferenceconvenience getters using reference-first fallback behavior:This behavior appears to be intentional. 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 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,
$refsiblings are evaluated additively. They are not overrides of the referenced schema.Example:
Both constraints apply. The sibling
minProperties: 2does not relax the target'sminProperties: 5.With the current object model:
This can make assertion keywords appear to override target constraints, even though JSON Schema evaluation treats them compositionally.
Question
Should
OpenApiSchemaReferencegetters continue to expose reference-first convenience behavior for all schema keywords, or should the library distinguish more explicitly between:JsonSchemaReferenceTargetPossible Directions
Option 1: Keep current behavior and document it
Keep
Reference.X ?? Target?.Xfor 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.ReferenceandschemaRef.Target.Option 2: Split annotation and assertion getter behavior
Keep reference-first getters for annotation-style keywords such as:
DescriptionTitleDefaultDeprecatedReadOnlyWriteOnlyExamplesExtensionsReturn target values for assertion/structural keywords such as:
TypeFormatMinPropertiesMaxPropertiesPatternRequiredPropertiesAllOfOneOfAnyOfNotItemsIfThenElseThis is more validation-oriented, but it breaks the uniform getter rule from #2903 and creates setter/getter asymmetry. For example, setting
schemaRef.Typestores the sibling value onReference, but readingschemaRef.Typewould 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 withallOfrather than flattening sibling assertions as overrides.Related Concern: Inline Serialization
CopyReferenceAsTargetElementWithOverrides()currently creates an inlineOpenApiSchemafrom 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:
This may be better handled separately from the getter behavior.
Related
$dynamicRefschemas.$dynamicRef/$dynamicAnchorresolution.