Repository navigation
openapi: a $ref to a property's schema reports readOnly as kept on a node that lacks it #750
Description
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
Unmodeledentry that is not in the document. - On
mainat 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
doneOn #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 fordefaultandwriteOnly. These are the three positions lowered throughschema.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
fix(compilers/openapi): let a declaration name what a $ref built
OmarAlJarrah commentedon Oct 8, 2026
format: passwordnow 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 givesorder-dependentdiagnostics inmorphic-harnesson that branch, wheremainreportsok.format: hex-tokenalready does so onmain.fuad-daoud commentedon Oct 11, 2026
#592 (
format: password) adds a case. A password property that writeswriteOnly,readOnlyordefaultnow owns a node, so a$refto its schema by pointer reports theopenapi/degraded-constructinfo only when the reference lowers first.Repro: the
a_first.yamldocument above withpreplaced by{type: string, format: password, writeOnly: true}.go run ./cmd/morphic-harnessreports itorder-dependenton diagnostics on that branch andokonmain.format: hex-tokenalready behaves this way onmain, so the fix here covers both.test(compilers/openapi): pin #750 through $anchor and $id references