Skip to content

fix: xml root element, root arrays and allOf composition - #223

Open
rishitank wants to merge 12 commits into
Redocly:mainfrom
rishitank:fix/xml-root-and-composition
Open

rishitank wants to merge 12 commits into
Redocly:mainfrom
rishitank:fix/xml-root-and-composition

Conversation

@rishitank

@rishitank rishitank commented Oct 14, 2026 •

Copy link
Copy Markdown

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 on main (1.7.8).

Case Before After
Root object with xml.name and a single property <foo>string</foo>: root and name lost <Thing><foo>string</foo></Thing>
Root xml.prefix / xml.namespace (non-$ref root) dropped: <Pet> <p:Pet xmlns:p="…">
Root array of strings <root><0>s</0><1>t</1><2>r</2>…</root> <root>string</root>
Root example array, 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>
Root object whose only property is an unwrapped array <book>…</book><book>…</book>: two document elements <root><book>…</book><book>…</book></root>
Property example/default array, e.g. tags: { type: 'array', items: { type: 'string' }, example: ['a', 'b'] } <0>a</0><1>b</1> <tags>a</tags><tags>b</tags>
Example array under an allOf, e.g. allOf: [$ref Tags] next to xml <root>a</root>…, or <0>a</0> inline <tags>a</tags><tags>b</tags>
Example array whose items is a $ref <0>…</0> on a property; two document elements at the root items named after the $ref, as when sampled
Wrapped example array with items.xml.name items named after the wrapper items named by items.xml.name
allOf: [$ref Base, {properties…}] extra <Base> wrapper around the ref's properties properties merged into one element
Root composed allOf without xml.name <root><p:Animal>…</p:Animal><age>…</age></root> <p:Animal xmlns:p="…">…<age>…</age></p:Animal>: named after its first $ref
Root array whose items are allOf: [$ref Item, {properties…}] one <item> with the other properties spilled beside it one <item> per item, with all of its properties
allOf: [$ref Pet] with its own xml: { name: 'Critter' } override ignored: <p:Animal> <Critter>…</Critter>
allOf: [$ref Base] next to the schema's own properties own properties spill into the parent one element with both
Composed allOf / inline oneOf object property its fields spill into the parent element its own <prop> element
oneOf property with a scalar first variant silently dropped <code>string</code>
allOf: [$ref] or oneOf: [$ref] property with its own example/default scalar dropped; object spills into the parent its own element
allOf: [$ref] + skipReadOnly (Redoc request samples) throws Cannot convert a Symbol value to a string readOnly props skipped
allOf: [$ref User, { properties: { tenant_id: { readOnly: true } } }] + skipReadOnly (the PatchUser schema from #140) throws Cannot convert a Symbol value to a string <PatchUser><username>string</username></PatchUser>
Unwrapped nullable property array (type: ['array', 'null']) <tags><tags>string</tags></tags> <tags>string</tags>
Root scalar: 0 / "" / true / "a" throws / throws / "" / <0>a</0> <root>0</root> etc.

Why existing snapshots change

  • Root element. In the OpenAPI 3.2.1 "XML Arrays" examples, a schema with xml: { name: document } and a single animals property serializes to <document>…</document>. The existing nodeType tests use those schemas. The sampler dropped that root whenever only one child element was left. Six snapshots are updated (the person, document ×4 and Item cases); their schemas are unchanged.
  • Two document elements. Two snapshots ("array with defined parent and item names" and "undefined parent and defined item names without wrapper") produced <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>.
  • Example arrays. Three snapshots are the example-array cases. "Defined parent and undefined item names" rendered <0>test1</0>. The two "defined item name" wrapped cases ignored items.xml.name.

Using each spec example's dataValue as the property example, all six OAS 3.2.1 "XML Arrays" examples now produce exactly the spec's XML. On main, none of them do.

How

  • convertJsonToXml:
    • A root schema with xml.name (and no nodeType other than element) names the document element, with its prefix and namespace.
    • Scalars and repeated elements are wrapped in root.
    • A root composed allOf without xml.name is named after its first $ref.
    • A root $ref is left alone, because traverse() already names it from the $ref and its siblings.
  • Literal arrays (example, default, const, enum, examples), at the root, on properties and under an allOf:
    • Items are named by items.xml.name, else by the $ref's xml.name or component name.
    • Otherwise unwrapped arrays use the property name (the array's own xml.name is ignored), and wrapped arrays use the array's own name.
    • A $ref that can't be resolved under an example (missing, external, or no spec) is skipped rather than looked up.
    • A null or scalar value of an array property keeps its element.
  • sampleArray: an unnamed scalar item takes the array's name (or root) instead of being spread into the result object, which is what split strings into characters. An unnamed composed allOf item is named after its first $ref.
  • allOfSample:
    • The children of a composed allOf are traversed with flattenXmlRef, so object $ref children merge into one element. A $ref typed as an array keeps its own nodes.
    • allOf: [$ref] with only annotation siblings (isAnnotatedRef) is still named like a plain $ref.
    • stripSkipped removes skipped properties nested under a ref wrapper. It copies instead of writing, so frozen specs work and examples are never modified.
  • resolveNodeType:
    • A composed allOf is an element, unless it is sampled as an array.
    • An allOf/oneOf/anyOf with its own value is an element.
    • Otherwise a oneOf/anyOf takes its node type from its first (sampled) variant.
    • type: ['array', 'null'] counts as an array. A schema without type: "array" keeps the element default, as the spec says.
  • resolveRef (utils) is shared by traverse() 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 to main for 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, with skipReadOnly, skipWriteOnly, skipNonRequired and maxSampleDepth.

Not changed (known limitations)

  • A root object without xml.name and with a single (non-array) property still renders without a wrapper. XML Node Names says an inline media-type schema MUST have an xml.name.
  • An unnamed scalar array nested directly in another array, or under additionalProperties, falls back to the element name root.
  • A oneOf whose first variant is { type: 'null' } renders an empty element. The spec suggests xsi:nil="true".
  • A root xml.prefix without a namespace produces an unbound prefix, as it already does for properties.
  • Literal values are not mapped through per-property XML settings:
    • an object example's properties don't become attributes or text nodes;
    • prefixItems names aren't applied to example arrays;
    • arrays nested directly inside an example array aren't named;
    • an item schema whose own example is an array still renders <0> (an existing snapshot, unchanged).
  • A sampled wrapped array with its own xml.name names unnamed items after the property, not the wrapper as the spec's last XML Arrays example does. This is unchanged from main; example arrays follow the spec.
  • An array of $ref items with minItems > 1 still renders a single item, as on main.
  • Objects with nodeType: 'text' or 'cdata' (not valid usage) are still not rendered correctly.
  • An allOf that mixes an array or scalar with an object (not valid; the sampler already warns) still renders badly, sometimes worse than on main.
  • A property $ref to an array component still gets an extra <root> element around its items, as on main.

Reference

Testing

  • npm test (oxlint + vitest): 236 tests pass, and npm run build is clean. The new XML tests are in test/xml.spec.js (68). The only changes to integration.spec.js are 11 updated snapshots. There are unit tests for stripSkipped, isArraySchema and isAnnotatedRef.
  • Run against 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.
  • Every snapshot in test/xml.spec.js parses with xmllint.
  • Fuzz: two generators, 34,000 XML cases in total (some deep-frozen), each compared with main.
    • No new throws.
    • No changes to input objects.
    • About 5,600 outputs went from malformed to well-formed.
    • The only outputs that went the other way are objects with nodeType: 'text'/'cdata', and allOfs mixing an array or scalar with an object (see the limitations).

Check yourself

  • Code is linted
  • Tested
  • All new/updated code is covered with tests

Security

  • Security impact of change has been considered
  • Code follows company security practices and guidelines

🤖 Generated with Claude Code

https://claude.ai/code/session_01117LuacuBxMJUn1wJkfdyL

rishitank and others added 2 commits October 14, 2026 17:05
- 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
rishitank and others added 3 commits October 15, 2026 00:08
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
rishitank added a commit to rishitank/.github that referenced this pull request Oct 15, 2026
…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
rishitank added a commit to rishitank/rishitank that referenced this pull request Oct 15, 2026
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
rishitank added a commit to rishitank/rishitank that referenced this pull request Oct 15, 2026
…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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trimmed to two sentences, in the style of the 1.7.7/1.7.8 entries.

Comment thread src/allOf.js Outdated
// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same comment already exists in utils.js:97. Additionally, some tests already describe this behavior so I'd dropt this comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment thread src/openapi-sampler.js Outdated
Comment on lines +18 to +21
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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread src/openapi-sampler.js Outdated
Comment on lines +30 to +31
// The root schema names the document element, whatever its content is
obj = { [prefixed(name, prefix)]: withNamespace(obj, prefix, namespace) };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment restates the condition, and the xml document root tests cover it

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment thread src/openapi-sampler.js Outdated
Comment on lines +52 to +54
function prefixed(name, prefix) {
return `${prefix ? prefix + ':' : ''}${name}`;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/utils.js
Comment on lines +109 to +123
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread src/utils.js Outdated
Comment on lines +154 to +158
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';
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment thread src/utils.js
Comment on lines +160 to +164
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';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// The first variant is the one that gets sampled, so it decides the node type would be enough

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, word for word.

Comment thread src/utils.js Outdated
Comment on lines 206 to 207
if (schema.example !== undefined) {
propertyName = schema.items?.xml?.name || propertyName;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread test/integration.spec.js Outdated
});
});

describe('xml document root', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@makare11a

Copy link
Copy Markdown
Contributor

Hello, @rishitank. Thanks, this will be a big improvement for XML samples. I suggested some improvements to your approach, please consider implementing them

rishitank and others added 7 commits October 19, 2026 18:43
- 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
… 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
@rishitank

Copy link
Copy Markdown
Author

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 main (#224–#228) into the branch and pushed the changes (c40b76b to 4c77b3b). There's a reply on each thread.

A few things beyond the threads, found while re-checking:

  • A regression from the merge, now fixed. With fix: apply keywords that are siblings of $ref #224/fix(realm): ref sibling #227, a root $ref with an xml sibling (the spec's StoredDocument example) got its document element twice. Only traverse() names a root $ref now, so the output matches main, including OpenAPI 3.0 ignoring the sibling.
  • Arrays without type: "array" are elements again. The spec gives nodeType a default of none only when type: "array" is present, so an untyped items schema is an element, rendered as main renders sampled ones. This PR had started treating it as an unwrapped array. type: ['array', 'null'] is still an array.
  • Examples whose $ref can't be resolved. traverse() never resolves a $ref under an example or default, so it can be missing or external. Item and root naming now fall back to the property name or root there, instead of throwing.
  • Two more data-loss fixes, which I'm happy to split out.
    • An allOf: [$ref] or oneOf: [$ref] property with its own example or default was dropped (a scalar) or spilled into its parent (an object), on main too.
    • The allOf items of a root array were merged into one element. They're now one element each, named after their first $ref.

Checks:

  • npm test passes (236 tests).
  • JSON output is identical to main for 31,720 schema and option combinations.
  • Two fuzzers (34,000 generated XML cases, some deep-frozen) found no new throws and no changes to input objects.
  • The only cases that went from well-formed to malformed are objects with nodeType: 'text' or 'cdata', and allOfs that mix an array or scalar with an object. Neither is valid usage (the sampler already warns about the second), and main doesn't render them correctly either.

I've also corrected the description, which overstated a couple of spec references.

@rishitank
rishitank requested a review from makare11a October 21, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants