Nested required object properties are seeded as top-level keys in the request body
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 697
- Forks
- 31
- Avg merge
- 3d 21h
- Merged PRs (30d)
- 10
Description
Summary
getJSONSchemaDefaults seeds an empty object for every property that is both listed in its parent's required array and typed object. It writes those to the root of the defaults object rather than to the property's own position in the tree. Those defaults are then merged with the caller's body, so a required object nested anywhere in a request schema is also sent as a spurious top-level key.
For an API that declares additionalProperties: false, this makes every affected request fail validation server-side.
Reproduction
const APICore = require('@readme/api-core').default;
const definition = {
openapi: '3.0.0',
info: { title: 'Example', version: '1.0' },
servers: [{ url: 'https://api.example.com' }],
paths: {
'/orders': {
post: {
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
additionalProperties: false,
required: ['reference', 'customer'],
properties: {
reference: { type: 'string' },
customer: {
type: 'object',
required: ['address'],
properties: {
address: {
type: 'object',
required: ['city'],
properties: { city: { type: 'string' } },
},
},
},
},
},
},
},
},
responses: { 200: { description: 'ok' } },
},
},
},
};
(async () => {
let sent;
global.fetch = async (input, init) => {
const request = input instanceof Request ? input : undefined;
sent = request ? await request.text() : init?.body;
return new Response('{}', {
status: 200,
headers: { 'content-type': 'application/json' },
});
};
const core = new APICore(definition);
core.setServer('https://api.example.com');
await core.fetch('/orders', 'post', {
reference: 'ORD-1',
customer: { address: { city: 'Berlin' } },
});
console.log(sent);
})();
Expected
{"reference":"ORD-1","customer":{"address":{"city":"Berlin"}}}
Actual
{"customer":{"address":{"city":"Berlin"}},"address":{},"reference":"ORD-1"}
address is required on customer, not on the root, but is sent at the root as {}.
Cause
In getJSONSchemaDefaults, the object-seeding branch writes to defaults directly, ignoring the destination the same block computes from parentPointer for the schema.default case just below it:
if (parentSchema?.required?.includes(String(indexProperty))) {
if (schema.type === 'object' && indexProperty) {
defaults[indexProperty] = {}; // <-- root, regardless of depth
}
let destination = defaults;
if (parentPointer) {
// ...walks parentPointer down to the correct nested position
}
if (schema.default !== undefined) {
destination[indexProperty] = schema.default; // <-- uses the nested position
}
}
The nesting-aware path already exists; it just isn't applied to the object branch.
Versions
Reproduced on @readme/api-core 7.0.0. The function is byte-identical in 7.0.2. The same code is present in api 6.x.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Locate getJSONSchemaDefaults in the TypeScript source and reproduce the nested required-object case from the issue. Trace how defaults are built for parentPointer and verify that the resulting request body contains the required object at its nested position without an extra top-level key.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100