Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"prebuild": "run-s -s clean lint",
"build": "tsc",
"postbuild": "chmod +x ./lib/rpc.js",
"lint:eslint": "eslint src/**/*.*",
"fix:eslint": "eslint --fix src/**/*.*",
"lint:eslint": "eslint \"src/**/*.*\"",
"fix:eslint": "eslint --fix \"src/**/*.*\"",
"lint:prettier": "prettier -c .",
"fix:prettier": "prettier -w .",
"clean:coverage": "rimraf coverage",
Expand Down
129 changes: 127 additions & 2 deletions src/json-schema-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
Enum,
EnumMember,
IntegerLiteral,
MapProperties,
MapValue,
MemberValue,
Parser,
Primitive,
Expand All @@ -20,7 +22,7 @@ import {
Violation,
} from 'basketry';
import parse = require('json-to-ast');
import { getName, resolve, LiteralNode, Literal } from './json';
import { getName, isLiteralNode, resolve, LiteralNode, Literal } from './json';
import * as AST from './json-schema';
import {
parseObjectValidationRules,
Expand Down Expand Up @@ -324,6 +326,7 @@ class JsonSchemaParser {
name,
description: toDescription(schema.description),
members,
disjunction: { kind: 'DisjunctionKindLiteral', value: 'exclusive' },
loc,
});
}
Expand Down Expand Up @@ -354,7 +357,28 @@ class JsonSchemaParser {
loc: string | undefined,
): MemberValue {
if (schema.anyOf) {
// TODO
const members: MemberValue[] = schema.anyOf.map(
(member) =>
this.parseType(member, encodeRange(0, member.loc)).memberValue,
);
const name = this.parseTypeName(schema);

if (!name) return untyped();

this.unions.set(name.value, {
kind: 'SimpleUnion',
name,
description: toDescription(schema.description),
members,
disjunction: { kind: 'DisjunctionKindLiteral', value: 'inclusive' },
loc,
});

return {
kind: 'ComplexValue',
typeName: name,
rules: [],
};
}

return untyped();
Expand Down Expand Up @@ -469,11 +493,14 @@ class JsonSchemaParser {
.map((child) => this.parseProperty(child))
.filter((prop): prop is Property => !!prop);

const mapProperties = this.parseMapProperties(schema, typeName);

this.types.set(typeName.value, {
kind: 'Type',
name: typeName,
description: toDescription(schema.description),
properties: properties || [],
mapProperties,
rules: Array.from(parseObjectValidationRules(schema)),
loc,
});
Expand Down Expand Up @@ -632,6 +659,104 @@ class JsonSchemaParser {
return untyped();
}

private parseMapProperties(
schema: AST.AbstractSchemaNode,
typeName: StringLiteral,
): MapProperties | undefined {
const ap = schema.additionalProperties;
if (!ap) return undefined;

// Handle boolean literal additionalProperties (true/false)
if (isLiteralNode(ap.node)) {
// false is handled by objectAdditionalPropertiesRule; true means untyped map

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

The comment says additionalProperties: false is handled by objectAdditionalPropertiesRule, but objectAdditionalPropertiesFactory currently returns undefined (TODO), so the parser won’t emit any rule/violation for additionalProperties: false. Either update this comment to reflect current behavior, or implement the missing additionalProperties rule so false is enforced.

Suggested change
// false is handled by objectAdditionalPropertiesRule; true means untyped map
// currently, only `true` produces an untyped map; `false` results in no MapProperties/rule

Copilot uses AI. Check for mistakes.
if (ap.node.value !== true) return undefined;
return {
kind: 'MapProperties',
key: {
kind: 'MapKey',
value: {
kind: 'PrimitiveValue',
typeName: { kind: 'PrimitiveLiteral', value: 'string' },
rules: [],
},
},
requiredKeys: [],
value: {
kind: 'MapValue',
value: untyped(),
},
};
}

return {
kind: 'MapProperties',
key: {
kind: 'MapKey',
value: {
kind: 'PrimitiveValue',
typeName: { kind: 'PrimitiveLiteral', value: 'string' },
rules: [],
},
},
requiredKeys: [],
value: this.parseMapValue(ap, typeName.value),
};
Comment on lines +662 to +703

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

parseMapProperties/parseMapValue add several behaviors (e.g., additionalProperties: { type: 'string' } without enum, and inline object schemas under additionalProperties) that aren’t exercised by the new spec tests. Adding at least one test for a primitive-typed map and one for an inline object map value would help prevent regressions in these branches.

Copilot uses AI. Check for mistakes.
}

private parseMapValue(
schema: AST.AbstractSchemaNode,
parentName: string,
): MapValue {
const isInline = !schema.ref;
const { memberValue } = this.parseType(schema, encodeRange(0, schema.loc));

if (isInline && memberValue.kind === 'ComplexValue') {
const autoName = memberValue.typeName.value;

const desiredName =
schema.oneOf || schema.anyOf
? `${parentName}MapValues`
: schema.enum
? `${parentName}MapValue`
: `${parentName}MapValues`;
Comment on lines +716 to +721

Copilot AI Mar 22, 2026

Copy link

Choose a reason for hiding this comment

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

desiredName’s fallback branch duplicates the union case (... : ${parentName}MapValues``), which makes the ternary harder to reason about and may be unintended (e.g., inline non-union complex schemas would also be renamed to *MapValues). Consider simplifying the conditional and/or choosing distinct singular/plural names for the non-union case to make the intent explicit.

Suggested change
const desiredName =
schema.oneOf || schema.anyOf
? `${parentName}MapValues`
: schema.enum
? `${parentName}MapValue`
: `${parentName}MapValues`;
const hasUnion = !!(schema.oneOf || schema.anyOf);
const desiredName =
schema.enum && !hasUnion
? `${parentName}MapValue`
: `${parentName}MapValues`;

Copilot uses AI. Check for mistakes.

if (autoName !== desiredName) {
if (this.enums.has(autoName)) {
const existing = this.enums.get(autoName)!;
this.enums.set(desiredName, {
...existing,
name: { kind: 'StringLiteral', value: desiredName },
});
this.enums.delete(autoName);
} else if (this.unions.has(autoName)) {
const existing = this.unions.get(autoName)!;
this.unions.set(desiredName, {
...existing,
name: { kind: 'StringLiteral', value: desiredName },
});
this.unions.delete(autoName);
} else if (this.types.has(autoName)) {
const existing = this.types.get(autoName)!;
this.types.set(desiredName, {
...existing,
name: { kind: 'StringLiteral', value: desiredName },
});
this.types.delete(autoName);
}

return {
kind: 'MapValue',
value: {
...memberValue,
typeName: { kind: 'StringLiteral', value: desiredName },
},
};
}
}

return { kind: 'MapValue', value: memberValue };
}

parseIsOptional(schema: AST.AbstractSchemaNode): TrueLiteral | undefined {
if (this.parseIsRequired(schema)) return undefined;

Expand Down
1 change: 1 addition & 0 deletions src/snapshot/snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -4363,6 +4363,7 @@
"rules": []
}
],
"disjunction": { "kind": "DisjunctionKindLiteral", "value": "exclusive" },
"loc": "0:426;5;432;6;9160;9360"
}
]
Expand Down
Loading
Loading