Skip to content

Commit f34cede

Browse files
nizzlenitzclaude
andcommitted
fix(openapi): carry nullability onto the top-level scalar schema
The scalar branch emitted the type alone, so a `static properties` field declared `{ type: 'string', nullable: true }` (or `['string','null']`, which folds to the same attribute) produced a document asserting the field rejects null — while MCP kept it nullable. The nested/array paths already translated this through toOpenApiDialect; only the top-level path did not. Also intersects `const` with a co-declared `enum` rather than deferring to the enum, matching the nested path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
1 parent 1eb8aa2 commit f34cede

2 files changed

Lines changed: 33 additions & 2 deletions

File tree

resources/openApi.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,27 @@ export function generateJsonApi(resources: Resources, serverHttpURL: string) {
251251
// subset: `const` only arrived in draft-06, so it is not a keyword here. Emit the
252252
// equivalent single-value `enum` instead of a keyword the declared dialect doesn't define.
253253
if (props[name] && typeof props[name] === 'object' && !('$ref' in props[name])) {
254-
const prop = props[name] as { description?: string; enum?: unknown; format?: string };
254+
const prop = props[name] as {
255+
description?: string;
256+
enum?: unknown[];
257+
format?: string;
258+
nullable?: boolean;
259+
};
255260
if (description) prop.description = description;
256261
if (attr.enum && prop.enum === undefined) prop.enum = attr.enum;
257262
// An author-declared `format` outranks the Harper type name `Type()` stamps on.
258263
if (attr.format) prop.format = attr.format;
259-
if (attr.const !== undefined && prop.enum === undefined) prop.enum = [attr.const];
264+
// Intersect rather than defer: `const` narrows an `enum` declared alongside it.
265+
if (attr.const !== undefined) {
266+
prop.enum = Array.isArray(prop.enum) ? prop.enum.filter((value) => value === attr.const) : [attr.const];
267+
}
268+
// `type: ['string', 'null']` folds to `type: 'string'` + `nullable` upstream; without this the
269+
// scalar path emits the type alone and the document claims the field rejects null.
270+
if (nullable) prop.nullable = true;
271+
// 3.0's `nullable` does not widen an `enum` — without `null` in the list a validator rejects it.
272+
if (prop.nullable && Array.isArray(prop.enum) && !prop.enum.includes(null)) {
273+
prop.enum = [...prop.enum, null];
274+
}
260275
}
261276
queryParamsArray.push(new Parameter(name, 'query', props[name]));
262277
}

unitTests/resources/openApi.test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,22 @@ describe('openApi — declared dialect compliance (3.0.3)', () => {
419419
expect(props.kind).to.not.have.property('const');
420420
});
421421

422+
it('carries nullability onto the emitted scalar schema', () => {
423+
// The walk assertions above only prove `type: 'null'` and unions are gone; they would pass just as
424+
// happily if nullability were dropped instead of translated.
425+
const props = buildDocument().components.schemas.Widget.properties;
426+
expect(props.maybe).to.deep.equal({ type: 'string', nullable: true });
427+
expect(props.nothing.nullable).to.equal(true);
428+
});
429+
430+
it('widens a nullable `enum` with `null` (3.0 `nullable` does not do it)', () => {
431+
const props = buildDocument().components.schemas.Widget.properties;
432+
expect(props.nullableEnum.nullable).to.equal(true);
433+
expect(props.nullableEnum.enum).to.deep.equal(['a', 'b', null]);
434+
// `const` + `nullable`: the single-value enum still has to admit null.
435+
expect(props.nullableConst.enum).to.deep.equal(['fixed', null]);
436+
});
437+
422438
it('emits the properties under test (guards the walk assertions against an empty document)', () => {
423439
const props = buildDocument().components.schemas.Widget.properties;
424440
for (const key of ['kind', 'nothing', 'maybe', 'nested', 'list', 'nullableEnum', 'nullableConst', 'when']) {

0 commit comments

Comments
 (0)