Skip to content

Clarify or revise OpenApiSchemaReference semantics for $ref sibling keywords #2919

Closed

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?.X

This 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: 2

Both 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;    // 5

This 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:

  • Description
  • Title
  • Default
  • Deprecated
  • ReadOnly
  • WriteOnly
  • Examples
  • Extensions

Return target values for assertion/structural keywords such as:

  • Type
  • Format
  • MinProperties
  • MaxProperties
  • Pattern
  • Required
  • Properties
  • AllOf
  • OneOf
  • AnyOf
  • Not
  • Items
  • If
  • Then
  • Else

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: 2

This may be better handled separately from the getter behavior.

Related

Activity

  1. aqeelat commented on Jul 1, 2026

    @aqeelat
    ContributorAuthor

    I dug into the recent history here and I think this needs maintainer direction before changing the getters.

    The current Reference.X ?? Target?.X behavior 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: 2 does not relax a target minProperties: 5.

    I see three possible directions (updated the issue body with details):

    1. Keep the current getter behavior and document it as an object-model convenience, not an evaluation/effective-schema view.
    2. 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.
    3. 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.

  2. handrews commented on Jul 2, 2026

    @handrews

    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.

  3. handrews commented on Jul 2, 2026

    @handrews

    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 like readOnly... if anything marks an instance location readOnly: true then it SHOULD (per the JSON Schema specification) be considered read-only even if some other subschema marks it readOnly: 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 minContains and maxContains technically function as "annotations" in the sense that they provide information to contains rather 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).

  4. baywet commented on Jul 2, 2026

    @baywet
    Member

    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?

  5. handrews commented on Jul 2, 2026

    @handrews

    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 under not as either the subschema failed (so no annotations) or it passed and then not caused 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 $ref or was it reached via a $ref, etc.), and that information can be used to determine the behavior. So then if there are two title annotations on a single instance location, and one was adjance to the $ref and 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 oneOf then 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.

  6. handrews commented on Jul 2, 2026

    @handrews

    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 $ref and 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."

  7. baywet commented on Jul 2, 2026

    @baywet
    Member

    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?

  8. aqeelat commented on Jul 2, 2026

    @aqeelat
    ContributorAuthor

    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 $ref schema 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
    • OpenApiSchemaReference convenience 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:

    1. Keep the existing reference-first convenience getter behavior for OpenApiSchemaReference.
    2. Explicitly document that these getters are not an effective/evaluation-oriented JSON Schema view.
    3. Tell consumers that need precise JSON Schema semantics to inspect both Reference and Target, or use a JSON Schema validator / future composed-schema API.
    4. 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 OpenApiSchemaReference rather than changing behavior. That would also give MicrosoftDocs/openapi-docs#168 a clear contract to document.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions