Skip to content

fix: avoid stray comma when required is set but properties is empty#841

Open
imalbi wants to merge 2 commits into
fastify:mainfrom
imalbi:fix/required-without-properties-extra-comma
Open

fix: avoid stray comma when required is set but properties is empty#841
imalbi wants to merge 2 commits into
fastify:mainfrom
imalbi:fix/required-without-properties-extra-comma

Conversation

@imalbi
Copy link
Copy Markdown

@imalbi imalbi commented May 19, 2026

Problem

A response schema with required set but no properties map (e.g. what z.toJSONSchema produces for z.record(enum, V)) serializes with a stray leading comma:

const schema = {
  type: 'object',
  properties: {
    obj: {
      type: 'object',
      propertyNames: { type: 'string', enum: ['a', 'b'] },
      additionalProperties: { type: 'number' },
      required: ['a', 'b']
    }
  }
}
build(schema)({ obj: { a: 1, b: 2 } })
// {"obj":{,"a":1,"b":2}}

HTTP 200 from Fastify, but JSON.parse fails client-side.

Root cause

The optimization at index.js:389 skips the runtime comma check when requiredProperties.length > 0, on the assumption that the first declared property will anchor the comma logic. When properties is empty (or absent), the loop body never runs and addComma is set to unconditional immediately after, so the additionalProperties branch at index.js:484 emits a separator before its first entry.

Fix

Tighten the condition to propertiesKeys.length > 0 && requiredProperties.includes(propertiesKeys[0]). The sort just above (index.js:367-373) moves required-and-declared keys to the front, so this captures exactly when the "first declared property anchors the comma" premise actually holds. It falls through to the existing runtime addComma flag path in three cases that the old condition handled incorrectly:

  1. properties is empty (record-style schema, only additionalProperties)
  2. required only names keys that are not in properties
  3. (Trivially) requiredProperties is empty — already handled before

No behaviour change for any schema where at least one declared property is required.

Tests

Three regression tests covering the canonical bug shapes:

  • test/additionalProperties.test.js: required + additionalProperties with no properties map (the original z.record-style trigger).
  • test/additionalProperties.test.js: required key not in properties, with additionalProperties: true.
  • test/patternProperties.test.js: same shape with patternProperties (symmetric branch at index.js:484).

Each test fails on the respective baseline (main for #1, the first proposed fix for #2 and #3) and passes after this patch. Full suite: 476/476 green.

Benchmark

Ran npm run benchmark 3 times on main and 3 times on the fix branch. All scenario ranges overlap; absolute mean deltas ≤ 1.1%, within natural inter-run variance (±0.7% to ±2.0%). The modified branch is not exercised by any standard scenario, so no runtime perf change is expected by construction.

After tightening the guard further (to also handle required keys not in properties), re-ran 3 vs 3 between the first fix and the tightened version. Absolute mean deltas ≤ 1.7%, still within inter-run variance. By transitivity, the tightened fix is also performance-neutral vs main.

Related


Checklist

The skip-leading-comma optimization assumes a declared property will
anchor the comma logic. With an empty properties map (e.g. a record-only
schema produced by z.toJSONSchema for z.record(enum, V)), the loop never
runs and the additionalProperties branch ends up writing a separator
before its first entry, producing '{ ,"k":v,... }' which fails JSON.parse.

Narrows the optimization condition to require both arrays to be non-empty;
otherwise falls back to the existing runtime addComma path.

Signed-off-by: Alberto Cerqua <106998309+imalbi@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 19, 2026 09:49
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes an invalid-JSON edge case in the object serializer where a schema can emit a leading comma when required is set but there are no declared properties, affecting record-style schemas using additionalProperties.

Changes:

  • Tighten the “skip first comma” optimization in buildInnerObject to avoid unconditional comma insertion when properties is empty.
  • Add a regression test covering required + additionalProperties with no declared properties on the nested object.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
index.js Adjusts comma-optimization guard in object serialization to prevent leading commas in an edge case.
test/additionalProperties.test.js Adds a regression test ensuring the affected schema shape produces valid JSON.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread index.js Outdated
Raised by auto-review on the PR: the previous guard
(`requiredProperties.length > 0 && propertiesKeys.length > 0`) is still
unsafe when `required` only names keys that are not declared in
`properties`. After sorting, `propertiesKeys[0]` is then not required,
so the "first declared property anchors the comma" premise does not
hold and the additionalProperties branch can still emit a stray
leading comma.

Tighten the condition to `propertiesKeys.length > 0 &&
requiredProperties.includes(propertiesKeys[0])`. The sort already
moves required-and-declared keys to the front, so this check captures
exactly the case where the optimization is safe. Falls through to the
runtime addComma path otherwise.

Regression test added for the new shape: `properties: { num }`,
`required: ['str']`, `additionalProperties: true`. Full suite green
(475/475). Re-ran benchmark 3 vs 3 against the previous fix, deltas
within inter-run noise, so by transitivity still performance-neutral
vs main.

Signed-off-by: Alberto Cerqua <106998309+imalbi@users.noreply.github.com>
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