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.
Summary
getJSONSchemaDefaultsseeds an empty object for every property that is both listed in its parent'srequiredarray and typedobject. 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
Expected
{"reference":"ORD-1","customer":{"address":{"city":"Berlin"}}}Actual
{"customer":{"address":{"city":"Berlin"}},"address":{},"reference":"ORD-1"}addressis required oncustomer, not on the root, but is sent at the root as{}.Cause
In
getJSONSchemaDefaults, the object-seeding branch writes todefaultsdirectly, ignoring thedestinationthe same block computes fromparentPointerfor theschema.defaultcase just below it:The nesting-aware path already exists; it just isn't applied to the object branch.
Versions
Reproduced on
@readme/api-core7.0.0. The function is byte-identical in 7.0.2. The same code is present inapi6.x.