From 435e9ae615ded65e434d2656e42815ed56647eb3 Mon Sep 17 00:00:00 2001 From: Andrea Carraro Date: Sun, 17 Aug 2025 21:14:39 +0200 Subject: [PATCH 1/6] chore: fix format issue on package.json --- README.md | 18 ++++++++++++++++++ package.json | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cd4fc00..61440ba 100644 --- a/README.md +++ b/README.md @@ -463,6 +463,24 @@ type Object = FromSchema; // => { [x: string]: unknown } ``` +- `FromSchema` comes with an `omitAdditionalProperties` option which will cause objects to be always typed as closed (without index signature type): + +```typescript +const openObjectSchema = { + type: "object", + additionalProperties: true, + patternProperties: { + foo: { type: "string" }, + }, +} as const; + +type Object = FromSchema< + typeof tupleSchema, + { omitAdditionalProperties: true } +>; +// => { foo: string } +``` + ## Combining schemas ### AnyOf diff --git a/package.json b/package.json index b65b620..a0bed6b 100644 --- a/package.json +++ b/package.json @@ -86,4 +86,4 @@ "url": "https://github.com/ThomasAribart/json-schema-to-ts/issues" }, "homepage": "https://github.com/ThomasAribart/json-schema-to-ts#readme" -} \ No newline at end of file +} From fe3213e741e0db5e852e246135d38d40e11ac27b Mon Sep 17 00:00:00 2001 From: Andrea Carraro Date: Sun, 17 Aug 2025 18:23:27 +0200 Subject: [PATCH 2/6] feat: add omitAdditionalProperties option type --- src/definitions/fromSchemaOptions.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/definitions/fromSchemaOptions.ts b/src/definitions/fromSchemaOptions.ts index 959215c..79288b1 100644 --- a/src/definitions/fromSchemaOptions.ts +++ b/src/definitions/fromSchemaOptions.ts @@ -15,6 +15,7 @@ export type FromSchemaOptions = { keepDefaultedPropertiesOptional?: boolean; references?: JSONSchemaReference[] | false; deserialize?: DeserializationPattern[] | false; + omitAdditionalProperties?: boolean; }; /** @@ -26,6 +27,7 @@ export type FromExtendedSchemaOptions = { keepDefaultedPropertiesOptional?: boolean; references?: ExtendedJSONSchemaReference[] | false; deserialize?: DeserializationPattern[] | false; + omitAdditionalProperties?: boolean; }; /** @@ -37,4 +39,5 @@ export type FromSchemaDefaultOptions = { keepDefaultedPropertiesOptional: false; references: false; deserialize: false; + omitAdditionalProperties: false; }; From 9382ac59aa90b3f122e41caaab0f6c2c74067b30 Mon Sep 17 00:00:00 2001 From: Andrea Carraro Date: Sun, 17 Aug 2025 19:28:48 +0200 Subject: [PATCH 3/6] test: add failing tests for omitAdditionalProperties option --- README.md | 4 +- src/parse-schema/object.unit.test.ts | 65 +++++++++++++++++++++++++++- src/tests/readme/object.type.test.ts | 26 +++++++++++ 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 61440ba..4314a68 100644 --- a/README.md +++ b/README.md @@ -469,7 +469,7 @@ type Object = FromSchema; const openObjectSchema = { type: "object", additionalProperties: true, - patternProperties: { + properties: { foo: { type: "string" }, }, } as const; @@ -478,7 +478,7 @@ type Object = FromSchema< typeof tupleSchema, { omitAdditionalProperties: true } >; -// => { foo: string } +// => { foo?: string } ``` ## Combining schemas diff --git a/src/parse-schema/object.unit.test.ts b/src/parse-schema/object.unit.test.ts index 5e8ad82..e85ab73 100644 --- a/src/parse-schema/object.unit.test.ts +++ b/src/parse-schema/object.unit.test.ts @@ -1,3 +1,5 @@ +import { expectTypeOf } from "expect-type"; + import type { FromSchema } from "~/index"; import { ajv } from "./ajv.util.test"; @@ -45,6 +47,17 @@ describe("Object schemas", () => { setInstance = { a: 42 }; expect(ajv.validate(setSchema, setInstance)).toBe(false); }); + + describe("with omitAdditionalProperties option", () => { + it("rejects additional properties", () => { + type Set = FromSchema< + typeof setSchema, + { omitAdditionalProperties: true } + >; + + expectTypeOf().toEqualTypeOf<{}>(); + }); + }); }); describe("Pattern properties", () => { @@ -95,6 +108,17 @@ describe("Object schemas", () => { objInstance = { B: true, S: "str", I: 42, N: null }; expect(ajv.validate(boolStrOrNumObjSchema, objInstance)).toBe(false); }); + + describe("with omitAdditionalProperties option", () => { + it("rejects object with boolean value", () => { + type Set = FromSchema< + typeof boolStrOrNumObjSchema, + { omitAdditionalProperties: true } + >; + + expectTypeOf().toEqualTypeOf<{}>(); + }); + }); }); describe("Unevaluated properties", () => { @@ -286,7 +310,7 @@ describe("Object schemas", () => { expect(ajv.validate(addressSchema, addressInstance)).toBe(true); }); - it("accepts object with missing required properties", () => { + it("rejects object with missing required properties", () => { // @ts-expect-error addressInstance = { number: 13, @@ -295,6 +319,26 @@ describe("Object schemas", () => { }; expect(ajv.validate(addressSchema, addressInstance)).toBe(false); }); + + describe("with omitAdditionalProperties option", () => { + it("rejects object with additional properties", () => { + type Address = FromSchema< + typeof addressSchema, + { omitAdditionalProperties: true } + >; + let addressInstance: Address; + + addressInstance = { + number: 13, + streetName: "Champs Elysées", + streetType: "Avenue", + direction: "NW", + // @ts-expect-error + additionalProperty: ["any", "value"], + }; + expect(ajv.validate(addressSchema, addressInstance)).toBe(true); + }); + }); }); describe("Required + Typed additional properties", () => { @@ -345,6 +389,25 @@ describe("Object schemas", () => { }; expect(ajv.validate(addressSchema, addressInstance)).toBe(false); }); + + describe("with omitAdditionalProperties option", () => { + it("rejects object with valid additional properties", () => { + type Address = FromSchema< + typeof addressSchema, + { omitAdditionalProperties: true } + >; + let addressInstance: Address; + + addressInstance = { + number: 13, + streetName: "Champs Elysées", + streetType: "Avenue", + // @ts-expect-error + additionalProperty: "additionalProperty", + }; + expect(ajv.validate(addressSchema, addressInstance)).toBe(true); + }); + }); }); describe("Required missing in properties", () => { diff --git a/src/tests/readme/object.type.test.ts b/src/tests/readme/object.type.test.ts index 774758e..9fdf281 100644 --- a/src/tests/readme/object.type.test.ts +++ b/src/tests/readme/object.type.test.ts @@ -168,3 +168,29 @@ type AssertObjectWithDefaultedProperty2 = A.Equals< >; const assertObjectWithDefaultedProperty2: AssertObjectWithDefaultedProperty2 = 1; assertObjectWithDefaultedProperty2; + +// With omitAdditionalProperties option + +const openObjectSchema2 = { + type: "object", + additionalProperties: true, + properties: { + foo: { type: "string" }, + }, +} as const; + +type ReceivedOpenObjectWithOmitAdditionalPropertiesOption = FromSchema< + typeof openObjectSchema2, + { omitAdditionalProperties: true } +>; +type ExpectedOpenObjectWithOmitAdditionalPropertiesOption = { + foo?: string; +}; + +type AssertOpenObjectWithOmitAdditionalPropertiesOption = A.Equals< + ReceivedOpenObjectWithOmitAdditionalPropertiesOption, + ExpectedOpenObjectWithOmitAdditionalPropertiesOption +>; + +const assertOpenObjectWithOmitAdditionalPropertiesOption: AssertOpenObjectWithOmitAdditionalPropertiesOption = 1; +assertOpenObjectWithOmitAdditionalPropertiesOption; From 3cb5697c784350e4f61c4da7be3362b2714e53cf Mon Sep 17 00:00:00 2001 From: Andrea Carraro Date: Sun, 17 Aug 2025 22:02:18 +0200 Subject: [PATCH 4/6] feat: implement omitAdditionalProperties option --- package.json | 1 + src/parse-options.ts | 3 +++ src/parse-options.type.test.ts | 1 + src/parse-schema/index.ts | 4 ++++ src/parse-schema/object.ts | 8 ++++++-- yarn.lock | 5 +++++ 6 files changed, 20 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a0bed6b..42e4102 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-prettier": "^5.0.1", "eslint-plugin-unused-imports": "^2.0.0", + "expect-type": "^1.2.2", "jest": "^27.5.1", "prettier": "^3.1.0", "rollup": "^2.67.3", diff --git a/src/parse-options.ts b/src/parse-options.ts index fc1442e..93c71ae 100644 --- a/src/parse-options.ts +++ b/src/parse-options.ts @@ -43,4 +43,7 @@ export type ParseOptions< deserialize: OPTIONS["deserialize"] extends DeserializationPattern[] | false ? OPTIONS["deserialize"] : FromSchemaDefaultOptions["deserialize"]; + omitAdditionalProperties: OPTIONS["omitAdditionalProperties"] extends boolean + ? OPTIONS["omitAdditionalProperties"] + : FromSchemaDefaultOptions["omitAdditionalProperties"]; }; diff --git a/src/parse-options.type.test.ts b/src/parse-options.type.test.ts index 7ed7ca2..d06862d 100644 --- a/src/parse-options.type.test.ts +++ b/src/parse-options.type.test.ts @@ -33,6 +33,7 @@ type ExpectedOptions = { deserialize: FromSchemaDefaultOptions["deserialize"]; rootSchema: RootSchema; references: IndexReferencesById; + omitAdditionalProperties: FromSchemaDefaultOptions["omitAdditionalProperties"]; }; const assertOptions: A.Equals = 1; diff --git a/src/parse-schema/index.ts b/src/parse-schema/index.ts index 6081b0b..78e9fba 100644 --- a/src/parse-schema/index.ts +++ b/src/parse-schema/index.ts @@ -47,6 +47,10 @@ export type ParseSchemaOptions = { * To override inferred types if some pattern is matched */ deserialize: DeserializationPattern[] | false; + /** + * Ignore additionalProperties value and always infer closed objects + */ + omitAdditionalProperties: boolean; }; /** diff --git a/src/parse-schema/object.ts b/src/parse-schema/object.ts index 44320bb..72f3ed1 100644 --- a/src/parse-schema/object.ts +++ b/src/parse-schema/object.ts @@ -41,13 +41,17 @@ export type ParseObjectSchema< }, GetRequired, GetOpenProps, - GetClosedOnResolve + OPTIONS["omitAdditionalProperties"] extends true + ? true + : GetClosedOnResolve > : M.$Object< {}, GetRequired, GetOpenProps, - GetClosedOnResolve + OPTIONS["omitAdditionalProperties"] extends true + ? true + : GetClosedOnResolve >; /** diff --git a/yarn.lock b/yarn.lock index 650262f..b5896f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3340,6 +3340,11 @@ exit@^0.1.2: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= +expect-type@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.2.2.tgz#c030a329fb61184126c8447585bc75a7ec6fbff3" + integrity sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA== + expect@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/expect/-/expect-27.5.1.tgz#83ce59f1e5bdf5f9d2b94b61d2050db48f3fef74" From d41d9920f3b2406b3456ebff7c85116f48a5cf59 Mon Sep 17 00:00:00 2001 From: Andrea Carraro Date: Mon, 18 Aug 2025 12:50:48 +0200 Subject: [PATCH 5/6] test: elisnt errors --- src/parse-schema/object.unit.test.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/parse-schema/object.unit.test.ts b/src/parse-schema/object.unit.test.ts index e85ab73..b75f47b 100644 --- a/src/parse-schema/object.unit.test.ts +++ b/src/parse-schema/object.unit.test.ts @@ -50,12 +50,12 @@ describe("Object schemas", () => { describe("with omitAdditionalProperties option", () => { it("rejects additional properties", () => { - type Set = FromSchema< + type Set2 = FromSchema< typeof setSchema, { omitAdditionalProperties: true } >; - expectTypeOf().toEqualTypeOf<{}>(); + expectTypeOf().toEqualTypeOf<{}>(); }); }); }); @@ -322,13 +322,12 @@ describe("Object schemas", () => { describe("with omitAdditionalProperties option", () => { it("rejects object with additional properties", () => { - type Address = FromSchema< + type Address2 = FromSchema< typeof addressSchema, { omitAdditionalProperties: true } >; - let addressInstance: Address; - addressInstance = { + const addressInstance2: Address2 = { number: 13, streetName: "Champs Elysées", streetType: "Avenue", @@ -336,7 +335,7 @@ describe("Object schemas", () => { // @ts-expect-error additionalProperty: ["any", "value"], }; - expect(ajv.validate(addressSchema, addressInstance)).toBe(true); + expect(ajv.validate(addressSchema, addressInstance2)).toBe(true); }); }); }); @@ -392,20 +391,19 @@ describe("Object schemas", () => { describe("with omitAdditionalProperties option", () => { it("rejects object with valid additional properties", () => { - type Address = FromSchema< + type Address2 = FromSchema< typeof addressSchema, { omitAdditionalProperties: true } >; - let addressInstance: Address; - addressInstance = { + const addressInstance2: Address2 = { number: 13, streetName: "Champs Elysées", streetType: "Avenue", // @ts-expect-error additionalProperty: "additionalProperty", }; - expect(ajv.validate(addressSchema, addressInstance)).toBe(true); + expect(ajv.validate(addressSchema, addressInstance2)).toBe(true); }); }); }); From b88d3148f37fa6ee073f4694b0a14efc39b9ee5f Mon Sep 17 00:00:00 2001 From: Andrea Carraro Date: Mon, 18 Aug 2025 13:39:19 +0200 Subject: [PATCH 6/6] refactor: GetClosedOnResolve generic --- src/parse-schema/object.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/parse-schema/object.ts b/src/parse-schema/object.ts index 72f3ed1..5c2b345 100644 --- a/src/parse-schema/object.ts +++ b/src/parse-schema/object.ts @@ -41,17 +41,13 @@ export type ParseObjectSchema< }, GetRequired, GetOpenProps, - OPTIONS["omitAdditionalProperties"] extends true - ? true - : GetClosedOnResolve + GetClosedOnResolve > : M.$Object< {}, GetRequired, GetOpenProps, - OPTIONS["omitAdditionalProperties"] extends true - ? true - : GetClosedOnResolve + GetClosedOnResolve >; /** @@ -112,8 +108,12 @@ type GetOpenProps< * @param OPTIONS Parsing options * @returns String */ -type GetClosedOnResolve = - OBJECT_SCHEMA extends Readonly<{ unevaluatedProperties: false }> +type GetClosedOnResolve< + OBJECT_SCHEMA extends ObjectSchema, + OPTIONS extends ParseSchemaOptions, +> = OPTIONS["omitAdditionalProperties"] extends true + ? true + : OBJECT_SCHEMA extends Readonly<{ unevaluatedProperties: false }> ? true : false;