fix: keep writeOnly fields out of response documentation (#160) - #180
Merged
clundie-CL merged 1 commit intoAug 27, 2026
Merged
Conversation
clundie-CL
marked this pull request as ready for review
August 24, 2026 15:43
clundie-CL
requested review from
benhepworth,
caubut-charter and
mayur007
as code owners
August 24, 2026 15:43
This was referenced Aug 26, 2026
…ct#160) Success-response documentation showed the writeOnly Wi-Fi passphrase (securityMode.password) via two independent paths: - Response examples in components/examples reused request-side YAML anchors. Split the anchors into request and response fragments; response fragments omit writeOnly fields. - Object-level `example:` blocks on the AccessDetail schemas embedded the password. An object-level example is context-free and is rendered verbatim into both the request and the response pane, bypassing writeOnly filtering. Removed every object-level example across the modules; renderers now synthesize object samples from property-level examples, which are readOnly/writeOnly aware. Backfilled the property-level examples that the removed blocks were masking. Also in this pass: - Drop the redundant readOnly overlay branches on TrustDomain, TrustDomainDevice, RebootRequest and NetworkAccessDevice. Those fields are already readOnly in ResourceIdentifier/ResourceAudit, and a bare `readOnly: true` stub wins the allOf merge and blanks the property to null in generated samples. TrustDomainDevice keeps the ipv4Address/ipv6Address overrides, which Commonalities Device does not declare readOnly, restated against the underlying schemas so they no longer blank. - Give ResourceIdentifier.id and ResourceAudit.modifiedAt their own property-level examples so synthesized samples stop repeating one literal for id/createdBy and one timestamp for createdAt/modifiedAt. - Remove duplicate example UUIDs that collided as schema identifiers and made redocly's no-invalid-schema-examples abort without validating five schemas. - Delete the unused thread-tlv-example anchor, which carried a `mode` property that ThreadTlvAccessDetail rejects. - Wire the three unreferenced components/examples into operations that had no example: RebootRequestResponseInferred and RebootRequestResponseExplicit on POST /reboot-requests, TrustDomainDeviceCreateAssignCredential on device registration. Reconcile the reboot request/response pairs: the response echoes the request's message verbatim, the immediate (inferred) response carries no atTime, and the scheduled time is later than the audit timestamps. - Add curated two-item list examples for GET /services and GET /network-access-devices, preserving the multi-item narratives the deleted object-level list examples carried (optional serviceSite omitted on one service; connected and disconnected deviceStatus values shown). - Promote no-invalid-media-type-examples and no-invalid-schema-examples to `error` in redocly.yaml. The former is the only check that validates examples with request/response context; readOnly/writeOnly are JSON Schema annotations rather than assertions, so the centralized Spectral pipeline cannot detect this class of defect. The latter validates schema-level examples structurally only. No API shape change.
clundie-CL
force-pushed
the
160-example-schema-guards
branch
from
August 26, 2026 17:17
b457306 to
fc1a8f5
Compare
This was referenced Aug 26, 2026
caubut-charter
approved these changes
Aug 27, 2026
caubut-charter
left a comment
Contributor
There was a problem hiding this comment.
LGTM. "Nit list" passed out of band for consideration but not a blocker.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #160 — resolves items 1, 4, and the comment-expanded items 5, 6, 8, 9 (item 3 was already fixed on
main).Item 2 (
404declared on theGET /network-access-devicescollection endpoint) is deliberately not addressed here: removing a documented response is an API-shape change, and this PR is shape-neutral by design. It is broken out into #188 and tracked there independently.What was wrong
The rendered documentation (Swagger UI and Redoc) showed the write-only Wi-Fi passphrase
securityMode.passwordin Trust Domain success-response samples, contradictingWpaPersonalDetail.passwordbeingwriteOnly: true.Two independent causes:
TrustDomainResponseBasicandTrustDomainResponseWiFiThreadspliced in the samewifi-wpa*-personal-examplefragments the create examples use.example:blocks on theAccessDetailschemas. This is the wider surface and the reasonGET /trust-domainsandPATCH /trust-domains/{id}leaked despite having no entry incomponents/examplesat all. An object-level example hangs off the schema, not off a request or a response, and renderers print it verbatim — so it shows in both panes with no read/write filtering.Why no linter caught it
readOnly/writeOnlyare JSON Schema annotations, not assertions. AJV — which Spectral validates examples with — ignores them, and Spectral has no request/response context, so a write-only secret in a200example is a valid instance of its schema. Confirmed by control experiment: injectingname: 12345into the same example is caught immediately byoas3-valid-media-example; the password is not. The centralized CAMARA pipeline is structurally incapable of detecting this class of defect.redocly lint'sno-invalid-media-type-examplesis context-aware and did flag cause (1) — but as a warning, andredocly lintexits 0. Nothing flagged cause (2).Approach
The three example altitudes behave differently, and the distinction is what was missed:
example:on a leaf propertyproperties— and that walk honorsreadOnly/writeOnlyexample:besideproperties/items/allOfexample:/examples:undercontent:Precedence is media type > object level > synthesis from property level, and it is strictly either/or: Redoc's
MediaTypeModelis anexamples ?? example ?? generatechain and Swagger UI passes a named example togetSampleSchemaas an override, so a synthesized sample never appears as an extra dropdown entry alongside curated ones. Where no media-type example exists, the renderer derives the sample from the schema plus its property-level examples — context-aware and always in sync. That is what the hand-maintained_imported_examplesanchor blocks were trying to build by hand.Curated media-type examples are therefore left untouched by this PR. They are the only way to control what a reader sees first, and synthesis is maximal — it populates every optional property — so it is not a substitute for a deliberately minimal "basic" example.
Object-level examples on arrays of scalars are kept — the array is the leaf, there are no properties beneath it to synthesize from and no read/write hazard.
Changes
example:across the modules and backfilled the property-level examples they were masking (MaxDomainDownstreamRatePolicy/MaxDomainUpstreamRatePolicyvalueandunit). Several of the removed blocks had already drifted: theservice-site-exampleanchor setlocation:to aPropertyAddressdirectly, thoughServiceSite.locationis{geographicPoint, propertyAddress}— the docs were publishing an invalid instance of the spec's own schema; thesupportedPoliciesexample listed 2 of 4 policies; thePoliciesexample contradicted the bandwidth values in themax-bandwidth-policy-exampleanchor.readOnlyoverlay branches onTrustDomain,TrustDomainDevice,RebootRequest,NetworkAccessDevice. Those fields are alreadyreadOnlyinResourceIdentifier/ResourceAudit, and a barereadOnly: truestub with notypewins theallOfmerge and blanks the property tonullin every generated sample.TrustDomainDevicekeeps itsipv4Address/ipv6Addressoverrides — CommonalitiesDevicedoes not declare those read-only — restated againstDeviceIpv4Address/DeviceIpv6Addressso they stop blanking.no-invalid-schema-examplesabort withresolves to more than one schema— silently skipping validation ofService,ServiceList,ServiceSite,NetworkAccessDevice,NetworkAccessDeviceList.thread-tlv-exampleanchor, which carried amodeproperty thatThreadTlvAccessDetailrejects underadditionalProperties: false.components/examplesinto operations that had none:RebootRequestResponseInferred/RebootRequestResponseExplicitonPOST /reboot-requests,TrustDomainDeviceCreateAssignCredentialon device registration. Reconciled the reboot request/response pairs while wiring them: the response now echoes the request'smessageverbatim, the immediate (inferred) response carries noatTime, and the scheduledatTimeis later thancreatedAt.GET /servicesandGET /network-access-devices. Deleting the object-level list examples would otherwise have cost their multi-item narratives (a service without the optionalserviceSite; bothconnectedanddisconnecteddevice states), since synthesis emits exactly one array element. These live at the media-type level, so the object-example rule still holds.ResourceIdentifier.idandResourceAudit.modifiedAttheir own property-level examples so synthesized samples stop showing one literal for bothidandcreatedBy, and one timestamp for bothcreatedAtandmodifiedAt.no-invalid-media-type-examplesandno-invalid-schema-examplestoerrorincode/redocly.yaml.No API shape change: no path, operation, schema, field, type, constraint, or scope is added, removed, or altered. The diff is examples, example plumbing, redundant
readOnlyrestatements, and lint severity.Verification
redocly lint(both specs, rules aterror)error=0 warning=0— identical to the pre-change baselineRendered output for
GET /trust-domains → 200, checked againstopenapi-sampler(the library Redoc uses):The request pane still shows
passwordand correctly omits the audit fields.Special notes for reviewers
TrustDomainResponseWiFiThreadstill containsnetworkKey, deliberately.ThreadStructuredAccessDetail.networkKeyis not markedwriteOnly, so the spec currently does say the server returns it. Changing the example without changing the schema would have made the example lie in the other direction. That is the substance of #124 and is left to it; the anchor carries a note to split the Thread fragment into request/response halves if those fields become write-only.Relatedly,
WiFiWpaEnterpriseAccessDetail,ThreadStructuredAccessDetailandThreadTlvAccessDetailalso had object-level examples, harmless only because nothing in them is currentlywriteOnly. Marking the Thread secrets write-only would have turned all three into live leaks. They are removed here, so #124 becomes a schema-only edit.On the severity promotion.
redocly lintreads config only from the working directory, andredocly.yamllives incode/. Linting fromcode/API_definitions/silently falls back to redocly's built-in defaults and does not apply these severities — worth knowing when reproducing locally:cd code redocly lint API_definitions/network-access-domains.yaml redocly lint API_definitions/network-access-devices.yaml