Skip to content

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

Description

@aqeelat

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, @baywet wrote after discussions with @handrews and @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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions