Skip to content

openapi: a $ref to a property's schema reports readOnly as kept on a node that lacks it #750

Description

@OmarAlJarrah

Problem

A schema $ref aimed at a property's, parameter's or header's schema lowers that position as a declaration with no carrier. When the position owns a node (an object, an array, an enum) and writes default, readOnly or writeOnly, the reference stores those keywords under the node's Unmodeled. It reports openapi/degraded-construct for each one: "it is kept verbatim under Unmodeled and applied to a referencing property, header or parameter". The declaration that owns the position lowers it as a carrier. That keeps the keywords on the ir.Property or ir.Parameter and stores nothing on the node.

So which lowering runs first decides the result:

  • On fix(compilers/openapi): let a declaration name what a $ref built #561, the declaration rebuilds a node that a reference built first, so the node holds no such entry in either order. But when the reference lowers first, the diagnostic it reported is still emitted. It describes an Unmodeled entry that is not in the document.
  • On main at 98ba293, the entries stay on the node and the diagnostic is reported when the reference lowers first. When the declaration lowers first, the reference resolves to the node it already built and adds neither. The registry differs between the two orders.

Reproduction

a_first.yaml:

openapi: 3.1.0
info: {title: t, version: "1"}
paths:
  /a:
    get:
      operationId: getA
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: {$ref: '#/paths/~1b/get/responses/200/content/application~1json/schema/properties/p'}
  /b:
    get:
      operationId: getB
      responses:
        "200":
          description: ok
          content:
            application/json:
              schema: {type: object, properties: {p: {type: object, readOnly: true, properties: {v: {type: string}}}}}

b_first.yaml is the same document with /b declared before /a.

for f in a_first b_first; do
  go run ./cmd/morphic compile $f.yaml > $f.json
  jq -c '.types["t/anon/paths/~1b/get/responses/200/content/application~1json/schema/properties/p"].unmodeled | keys? // null' $f.json
done

On #561, a_first prints the openapi/degraded-construct info diagnostic for /properties/p/readOnly and null. b_first prints no diagnostic and null. On main, a_first prints the diagnostic and ["openapi:readOnly"]. go run ./cmd/morphic-harness reports both files order-dependent: on #561 for their diagnostics, on main for their registry.

Notes

  • The same holds for a parameter's schema (/paths/~1b/get/parameters/0/schema) and a response header's (.../headers/X-H/schema), and for default and writeOnly. These are the three positions lowered through schema.CarriedRef.
  • On fix(compilers/openapi): let a declaration name what a $ref built #561 the cause is general: the declaration's rebuild replaces what a reference built, not what it reported. openapi: a $ref and the declaration it targets reach the schema depth bound at different coordinates #748 is the same gap at the depth bound. A fix could hold a reference's report about a coordinate until it is known whether the coordinate's declaration rebuilds it. Alternatively, it could settle that a referenced carrier position's node holds these keywords in every order. Neither should drop them from a position that only references reach, such as one beneath $defs, where the reference's record is the only one.

Activity

  1. OmarAlJarrah commented on Oct 8, 2026

    @OmarAlJarrah
    MemberAuthor

    format: password now reaches this path as well, because a password position owns a node after #592. The usual spelling is {type: string, format: password, writeOnly: true}. Replaying the repro above with that property gives order-dependent diagnostics in morphic-harness on that branch, where main reports ok. format: hex-token already does so on main.

  2. fuad-daoud commented on Oct 11, 2026

    @fuad-daoud
    Collaborator

    #592 (format: password) adds a case. A password property that writes writeOnly, readOnly or default now owns a node, so a $ref to its schema by pointer reports the openapi/degraded-construct info only when the reference lowers first.

    Repro: the a_first.yaml document above with p replaced by {type: string, format: password, writeOnly: true}. go run ./cmd/morphic-harness reports it order-dependent on diagnostics on that branch and ok on main. format: hex-token already behaves this way on main, so the fix here covers both.

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

    priority:P4Off the first emitter's pathtype:bugDefect in shipped behavior

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions