Repository navigation
Conversation
- Name the document element after the root schema's xml.name, with its prefix and namespace, also when the object has a single property (as in the OpenAPI 3.2 `<document>` examples). - Stop splitting unnamed scalar array items into one element per character, keep several unnamed root items under one root element and treat nullable or untyped arrays as arrays. - Merge $refs composed in an allOf into one element; keep a single (optionally annotated) $ref named like a plain $ref. - Render oneOf/anyOf properties whose first variant is an inline object, a composed allOf or a scalar as their own element. - Strip skipped readOnly/writeOnly properties nested under a $ref wrapper in an allOf instead of throwing. - Wrap root scalar samples in a root element. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL
Six existing nodeType snapshots now include the root element named by the root schema's xml.name, matching the corresponding examples in the OpenAPI 3.2 specification (`<document>…</document>`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL
From a review of the XML fixes: - An allOf of a single $ref that adds its own xml or properties is merged and named by the schema, instead of nesting the $ref's element inside it. - A composed allOf that samples as an array is no longer wrapped twice. - isArraySchema uses inferType, the type resolution sampling uses, so a property typed ['string', 'array'] is no longer dropped. - A root composed allOf without xml.name is named after its first $ref (its xml.name, prefix and namespace, or the component name). - Root example arrays produce one well-formed document element, including with a namespace and for wrapped arrays. - Repeated root elements are always kept under one root element; two existing snapshots that produced two document elements now wrap them. - Tests: the cases above, unwrapped nullable and untyped property arrays, the issue Redocly#140 PatchUser + skipReadOnly case, the root null throw, a null-first oneOf, nested and additionalProperties arrays, and unit tests for stripSkipped, isArraySchema and isAnnotatedRef. 205 tests, 73 snapshots; JSON output unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL
Completes the previous commit: the isAnnotatedRef, isArraySchema and resolveNodeType changes it imports, and the integration tests for every fix (two existing snapshots that produced two document elements now wrap them in one root element). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL
… arrays The integration tests for the two previous commits: one per fix, the unwrapped nullable and untyped property arrays, the issue Redocly#140 PatchUser case with skipReadOnly, the root null throw, a null-first oneOf, nested and additionalProperties arrays. Two existing snapshots that produced two document elements now wrap them in one root element, as XML 1.0 requires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL
…r echoes From the independent review of #15: - A fork branch whose pull request is still open upstream is refused: open PRs with this branch as head are also looked up in the fork's parent and source repositories (rishitank/openapi-sampler's branch for the open Redocly/openapi-sampler#223 would otherwise have passed every check). - After the DELETE only a 404 counts as gone; any other read-back failure fails the run, with a short retry for a read that briefly still sees it. - The job only runs from main, so a dispatch on another branch cannot run an unreviewed copy of the script with PRUNE_TOKEN. - A renamed or transferred repository, or a renamed branch, that gh follows through a redirect is refused. Owner names compare case-insensitively. - Inputs are stripped of newlines and backticks before they are echoed, so a value cannot start its own workflow command. A flag without a value is a usage error (exit 2). - The header and workflow say the tip check narrows, but cannot close, the window before the DELETE. 40 stub checks pass; each new guard is mutation-tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL
Redocly/redoc#2347 (2023) was closed when Redoc v3 shipped. Its CSV half was re-raised against v3 as Redocly/redoc#2869 and merged on 15 Oct 2026; the XML half became fixes to openapi-sampler, Redocly/openapi-sampler#223, still in review. Same wording as the portfolio's open-source card. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL
…es await review (#2) The Redoc line still described the 2023 PR (Redocly/redoc#2347) as open. It now says the examples were first proposed in 2023 and later requested by other developers, CSV is merged into v3 for the next release (Redocly/redoc#2869, merged 15 Oct 2026, after redoc@3.0.1), and the XML fixes to openapi-sampler await review (Redocly/openapi-sampler#223). Reviewed by CodeRabbit (approved 564170d) and an independent Claude fact-check of every claim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL
There was a problem hiding this comment.
Could we trim this to 2-3 lines? Changesets are copied word for word into CHANGELOG.md, and the detailed list already lives in the PR description and the tests
There was a problem hiding this comment.
Trimmed to two sentences, in the style of the 1.7.7/1.7.8 entries.
| // In XML, a $ref is normally wrapped in an element named after it. The children of a | ||
| // composed allOf describe one element together, so their properties are merged flat. | ||
| // A lone $ref keeps its wrapper, so it is named like a plain $ref would be, unless the | ||
| // schema adds properties or its own xml to it: then it is composed like any other allOf. |
There was a problem hiding this comment.
The same comment already exists in utils.js:97. Additionally, some tests already describe this behavior so I'd dropt this comment
| const isComposed = !!schema?.allOf && !isAnnotatedRef(schema); | ||
| // A composed allOf merges its $refs into one element, so without a name of its own | ||
| // the document element is named after its first $ref, like that $ref alone would be | ||
| const { name, prefix, namespace } = getXMLAttributes(schema?.xml?.name || !isComposed ? schema : firstRefXml(schema.allOf, spec)); |
There was a problem hiding this comment.
This condition requires two-line comment before anyone can read it. Please consider creating an intermediate value:
const isComposed = !!schema?.allOf && !isAnnotatedRef(schema);
const xmlSource = isComposed && !schema.xml?.name ? firstRefXml(schema.allOf, spec) : schema;
const { name, prefix, namespace } = getXMLAttributes(xmlSource);There was a problem hiding this comment.
Done, with your intermediate values, and isComposedAllOf is now a small helper in utils because applyXMLAttributes needs it too.
One addition: a root $ref is excluded first (const rootSchema = schema?.$ref ? undefined : schema). After #224/#227, traverse() already names it from the $ref and its siblings, so naming it again doubled the element (e.g. the spec's StoredDocument example).
| // The root schema names the document element, whatever its content is | ||
| obj = { [prefixed(name, prefix)]: withNamespace(obj, prefix, namespace) }; |
There was a problem hiding this comment.
The comment restates the condition, and the xml document root tests cover it
| function prefixed(name, prefix) { | ||
| return `${prefix ? prefix + ':' : ''}${name}`; | ||
| } |
There was a problem hiding this comment.
prefix:name is now built in four places: here, samplers/array.js:34, utils.js:180, and for xmlns. Could prefixed move to utils.js and be reused?
There was a problem hiding this comment.
Moved to utils as prefixed(name, prefix), and used in convertJsonToXml, literalArrayToXml, sampleArray and applyXMLAttributes. The xmlns:prefix attribute is now built in one place, withNamespace.
| export function stripSkipped(value) { | ||
| if (Array.isArray(value)) { | ||
| return value.map(stripSkipped); | ||
| } | ||
| if (value && typeof value === 'object') { | ||
| for (const key of Object.keys(value)) { | ||
| if (value[key] === SKIP_SYMBOL) { | ||
| delete value[key]; | ||
| } else { | ||
| value[key] = stripSkipped(value[key]); | ||
| } | ||
| } | ||
| } | ||
| return value; | ||
| } |
There was a problem hiding this comment.
With a deep-frozen spec, sample(schema, { format: 'xml' }, spec) now throws Cannot assign to read only property 'tags'. With a normal spec, it silently replaces arrays inside the user’s examples. value[key] = stripSkipped(value[key]) also runs on plain example objects, because mergeDeep keeps references to them, and for arrays it assigns a new copy. Writing only when there’s something to delete should fix it
There was a problem hiding this comment.
Thanks, confirmed. It happened even without skipReadOnly, for any XML allOf whose example held an object with an array in it. stripSkipped now copies only the objects it removes something from and returns everything else as is, so it never writes to the spec. The tests sample a deep-frozen schema and spec without throwing, and check that the example's objects and arrays are untouched (xml spec objects in test/xml.spec.js, plus unit tests).
| if (schema?.allOf && !isAnnotatedRef(schema)) { | ||
| // A composed allOf is one element, unless it was sampled as an array: then, like any | ||
| // other array, its items are the nodes | ||
| return resultType === 'array' ? 'none' : 'element'; | ||
| } |
There was a problem hiding this comment.
The ternary says exactly this, and “should not wrap a composed allOf that is sampled as an array twice” test covers it, please consider dropping this comment
| const variants = schema?.oneOf || schema?.anyOf; | ||
| if (Array.isArray(variants) && variants.length) { | ||
| // The sample is taken from the first variant, so it decides the node type: a $ref | ||
| // or array variant names its own nodes, any other variant is this property's element. | ||
| return resolveNodeType(variants[0], resultType) === 'element' ? 'element' : 'none'; |
There was a problem hiding this comment.
// The first variant is the one that gets sampled, so it decides the node type would be enough
| if (schema.example !== undefined) { | ||
| propertyName = schema.items?.xml?.name || propertyName; |
There was a problem hiding this comment.
A literal array on a property still produces <0>a</0><1>b</1>, which isn’t well-formed XML. For example, { tags: { type: 'array', items: { type: 'string' }, example: ['a', 'b'] } } gives <Doc><0>a</0><1>b</1></Doc>, and default isn’t handled here at all
There was a problem hiding this comment.
Fixed for example and default, and for const, enum and examples too. An unwrapped literal array on a property now names its items by items.xml.name, else by the $ref's name, else by the property name. The array's own xml.name is ignored, as the name field says for nodeType none. A wrapped literal array names unnamed items after itself, as in the spec's last XML Arrays example. Your example now gives <Doc><tags>a</tags><tags>b</tags></Doc>.
The same applies to an example array reached through an allOf, e.g. allOf: [$ref Tags] next to xml (the 3.0 way to annotate a $ref). That rendered <root>a</root><root>b</root> on main, and <0>a</0> for an inline array.
This changes three existing snapshots in integration.spec.js; their schemas are unchanged. One was pinning <0>test1</0>. The other two were wrapped arrays that ignored items.xml.name (<booksLibrary><booksLibrary> instead of <booksLibrary><book>). I also used each example's dataValue as the property example for the six OAS 3.2.1 "XML Arrays" examples, and all six now produce exactly the spec's XML.
| }); | ||
| }); | ||
|
|
||
| describe('xml document root', () => { |
There was a problem hiding this comment.
This adds about 880 lines to a file that was already about 2,000 lines. Could the new describe blocks move to test/xml.spec.js or something like that?
There was a problem hiding this comment.
Moved. The six new describe blocks and the new tests are in test/xml.spec.js (68 tests). The only changes left in integration.spec.js are the 11 updated snapshots.
This comment was marked as duplicate.
This comment was marked as duplicate.
Sorry, something went wrong.
|
Hello, @rishitank. Thanks, this will be a big improvement for XML samples. I suggested some improvements to your approach, please consider implementing them |
…composition Brings in Redocly#224, Redocly#226, Redocly#227 and Redocly#228. No conflicts.
- Share $ref resolution (resolveRef, with Redocly#224 sibling merging) between traverse() and the XML root naming; drop the duplicate lookup and catch. - A root $ref is named once by traverse(), from the $ref and its siblings. - stripSkipped copies instead of writing to spec objects, so frozen specs work and examples are never modified. - example/default arrays on properties name their items like sampled arrays (items xml.name or $ref, else the property name) instead of <0>, <1>. - Root example arrays name $ref items after the $ref. - allOf/oneOf with their own example, default, const or enum render as an element instead of being dropped. - Move prefixed/withNamespace to utils and reuse them; trim comments and the changeset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL
… fixes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL
… array items The new XML describe blocks now live in test/xml.spec.js. Three existing snapshots change, schemas unchanged: wrapped example arrays use the items' xml.name (as sampled arrays do), and an unwrapped example array no longer renders <0>/<1> but repeats the property element. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL
…ples, composed array items Found in a second independent review of the previous push: - An example array under an allOf (e.g. allOf: [$ref] next to xml, the 3.0 way to annotate a $ref) rendered <0>/<1>: it is now named like other literal arrays. An array $ref in a composed allOf keeps its own nodes. - A $ref under an example or default is never resolved by traverse(), so it can be missing or external: refXml now returns null for those instead of throwing, and the element falls back to the property name or root. - A null or scalar value of an array property keeps its element, as on main. - Composed allOf items of a root array are named after their first $ref instead of being merged into one element. - Arrays without `type: "array"` keep the element nodeType the spec gives them (as on main); only the root naming uses the sampled type. - Tidy: one import line from utils in traverse.js, shorter destructuring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL
…are elements Untyped `items` schemas keep the element nodeType the spec gives them (only `type: "array"` defaults to none), as on main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL
|
Thanks for such a careful review, @makare11a. The frozen-spec and property-example-array points were real bugs, and the rest made the code a lot clearer. I've merged A few things beyond the threads, found while re-checking:
Checks:
I've also corrected the description, which overstated a couple of spec references. |
What/Why/How?
Redoc v3 renders its XML examples with
sample(schema, { format: 'xml' }, spec). I ported my old Redoc PR (Redocly/redoc#2347, XML/CSV examples) to v3 and compared its XML with Redoc v3's. I found cases where the XML sample is not well-formed, throws, or drops data. Each "Before" below is reproduced onmain(1.7.8).xml.nameand a single property<foo>string</foo>: root and name lost<Thing><foo>string</foo></Thing>xml.prefix/xml.namespace(non-$refroot)<Pet><p:Pet xmlns:p="…"><root><0>s</0><1>t</1><2>r</2>…</root><root>string</root>examplearray, e.g.{ xml: { name: 'X' }, example: ['a', 'b'] }<X>a</X><X>b</X>: two document elements<X><X>a</X><X>b</X></X><book>…</book><book>…</book>: two document elements<root><book>…</book><book>…</book></root>example/defaultarray, e.g.tags: { type: 'array', items: { type: 'string' }, example: ['a', 'b'] }<0>a</0><1>b</1><tags>a</tags><tags>b</tags>allOf, e.g.allOf: [$ref Tags]next toxml<root>a</root>…, or<0>a</0>inline<tags>a</tags><tags>b</tags>itemsis a$ref<0>…</0>on a property; two document elements at the root$ref, as when sampleditems.xml.nameitems.xml.nameallOf: [$ref Base, {properties…}]<Base>wrapper around the ref's propertiesallOfwithoutxml.name<root><p:Animal>…</p:Animal><age>…</age></root><p:Animal xmlns:p="…">…<age>…</age></p:Animal>: named after its first$refallOf: [$ref Item, {properties…}]<item>with the other properties spilled beside it<item>per item, with all of its propertiesallOf: [$ref Pet]with its ownxml: { name: 'Critter' }<p:Animal><Critter>…</Critter>allOf: [$ref Base]next to the schema's ownpropertiesallOf/ inlineoneOfobject property<prop>elementoneOfproperty with a scalar first variant<code>string</code>allOf: [$ref]oroneOf: [$ref]property with its ownexample/defaultallOf: [$ref]+skipReadOnly(Redoc request samples)allOf: [$ref User, { properties: { tenant_id: { readOnly: true } } }]+skipReadOnly(thePatchUserschema from #140)<PatchUser><username>string</username></PatchUser>type: ['array', 'null'])<tags><tags>string</tags></tags><tags>string</tags>0/""/true/"a"""/<0>a</0><root>0</root>etc.Why existing snapshots change
xml: { name: document }and a singleanimalsproperty serializes to<document>…</document>. The existingnodeTypetests use those schemas. The sampler dropped that root whenever only one child element was left. Six snapshots are updated (theperson,document×4 andItemcases); their schemas are unchanged.<book>test1</book><book>test2</book>. That is two document elements, so it is not well-formed XML (XML 1.0, §2.1, production [1]). They are now wrapped in<root>.<0>test1</0>. The two "defined item name" wrapped cases ignoreditems.xml.name.Using each spec example's
dataValueas the propertyexample, all six OAS 3.2.1 "XML Arrays" examples now produce exactly the spec's XML. Onmain, none of them do.How
convertJsonToXml:xml.name(and nonodeTypeother thanelement) names the document element, with itsprefixandnamespace.root.allOfwithoutxml.nameis named after its first$ref.$refis left alone, becausetraverse()already names it from the$refand its siblings.example,default,const,enum,examples), at the root, on properties and under anallOf:items.xml.name, else by the$ref'sxml.nameor component name.xml.nameis ignored), and wrapped arrays use the array's own name.$refthat can't be resolved under an example (missing, external, or no spec) is skipped rather than looked up.sampleArray: an unnamed scalar item takes the array's name (orroot) instead of being spread into the result object, which is what split strings into characters. An unnamed composedallOfitem is named after its first$ref.allOfSample:allOfare traversed withflattenXmlRef, so object$refchildren merge into one element. A$reftyped as an array keeps its own nodes.allOf: [$ref]with only annotation siblings (isAnnotatedRef) is still named like a plain$ref.stripSkippedremoves skipped properties nested under a ref wrapper. It copies instead of writing, so frozen specs work and examples are never modified.resolveNodeType:allOfis anelement, unless it is sampled as an array.allOf/oneOf/anyOfwith its own value is anelement.oneOf/anyOftakes its node type from its first (sampled) variant.type: ['array', 'null']counts as an array. A schema withouttype: "array"keeps theelementdefault, as the spec says.resolveRef(utils) is shared bytraverse()and the XML naming, including the fix: apply keywords that are siblings of $ref #224 sibling merge.Every behavioural change is behind
format === 'xml'. JSON output is identical tomainfor 31,720 schema and option combinations, generated by two separate harnesses. They cover OpenAPI 3.0, 3.1 and 3.2, Swagger 2 and no version, withskipReadOnly,skipWriteOnly,skipNonRequiredandmaxSampleDepth.Not changed (known limitations)
xml.nameand with a single (non-array) property still renders without a wrapper. XML Node Names says an inline media-type schema MUST have anxml.name.additionalProperties, falls back to the element nameroot.oneOfwhose first variant is{ type: 'null' }renders an empty element. The spec suggestsxsi:nil="true".xml.prefixwithout anamespaceproduces an unbound prefix, as it already does for properties.prefixItemsnames aren't applied to example arrays;<0>(an existing snapshot, unchanged).xml.namenames unnamed items after the property, not the wrapper as the spec's last XML Arrays example does. This is unchanged frommain; example arrays follow the spec.$refitems withminItems> 1 still renders a single item, as onmain.nodeType: 'text'or'cdata'(not valid usage) are still not rendered correctly.allOfthat mixes an array or scalar with an object (not valid; the sampler already warns) still renders badly, sometimes worse than onmain.$refto an array component still gets an extra<root>element around its items, as onmain.Reference
PatchUserschema no longer throws when sampled as XML withskipReadOnly. JSON samples already honoured it.name,nodeType), XML Node Names, and the XML Arrays examples.Testing
npm test(oxlint + vitest): 236 tests pass, andnpm run buildis clean. The new XML tests are intest/xml.spec.js(68). The only changes tointegration.spec.jsare 11 updated snapshots. There are unit tests forstripSkipped,isArraySchemaandisAnnotatedRef.main, 68 of the 236 tests fail: 52 of the 68 XML tests, the 11 updated snapshots, and 5 unit tests for the new helpers.test/xml.spec.jsparses withxmllint.main.nodeType: 'text'/'cdata', andallOfs mixing an array or scalar with an object (see the limitations).Check yourself
Security
🤖 Generated with Claude Code
https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL