Skip to content
Open
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,24 @@ type Object = FromSchema<typeof openObjectSchema>;
// => { [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,
properties: {
foo: { type: "string" },
},
} as const;

type Object = FromSchema<
typeof tupleSchema,
{ omitAdditionalProperties: true }
>;
// => { foo?: string }
```

## Combining schemas

### AnyOf
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -86,4 +87,4 @@
"url": "https://github.com/ThomasAribart/json-schema-to-ts/issues"
},
"homepage": "https://github.com/ThomasAribart/json-schema-to-ts#readme"
}
}
3 changes: 3 additions & 0 deletions src/definitions/fromSchemaOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type FromSchemaOptions = {
keepDefaultedPropertiesOptional?: boolean;
references?: JSONSchemaReference[] | false;
deserialize?: DeserializationPattern[] | false;
omitAdditionalProperties?: boolean;
};

/**
Expand All @@ -26,6 +27,7 @@ export type FromExtendedSchemaOptions<EXTENSION extends JSONSchemaExtension> = {
keepDefaultedPropertiesOptional?: boolean;
references?: ExtendedJSONSchemaReference<EXTENSION>[] | false;
deserialize?: DeserializationPattern[] | false;
omitAdditionalProperties?: boolean;
};

/**
Expand All @@ -37,4 +39,5 @@ export type FromSchemaDefaultOptions = {
keepDefaultedPropertiesOptional: false;
references: false;
deserialize: false;
omitAdditionalProperties: false;
};
3 changes: 3 additions & 0 deletions src/parse-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
};
1 change: 1 addition & 0 deletions src/parse-options.type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type ExpectedOptions = {
deserialize: FromSchemaDefaultOptions["deserialize"];
rootSchema: RootSchema;
references: IndexReferencesById<AllReferences>;
omitAdditionalProperties: FromSchemaDefaultOptions["omitAdditionalProperties"];
};

const assertOptions: A.Equals<ReceivedOptions, ExpectedOptions> = 1;
Expand Down
4 changes: 4 additions & 0 deletions src/parse-schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down
12 changes: 8 additions & 4 deletions src/parse-schema/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ export type ParseObjectSchema<
},
GetRequired<OBJECT_SCHEMA, OPTIONS>,
GetOpenProps<OBJECT_SCHEMA, OPTIONS>,
GetClosedOnResolve<OBJECT_SCHEMA>
GetClosedOnResolve<OBJECT_SCHEMA, OPTIONS>
>
: M.$Object<
{},
GetRequired<OBJECT_SCHEMA, OPTIONS>,
GetOpenProps<OBJECT_SCHEMA, OPTIONS>,
GetClosedOnResolve<OBJECT_SCHEMA>
GetClosedOnResolve<OBJECT_SCHEMA, OPTIONS>
>;

/**
Expand Down Expand Up @@ -108,8 +108,12 @@ type GetOpenProps<
* @param OPTIONS Parsing options
* @returns String
*/
type GetClosedOnResolve<OBJECT_SCHEMA extends ObjectSchema> =
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;

Expand Down
63 changes: 62 additions & 1 deletion src/parse-schema/object.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { expectTypeOf } from "expect-type";

import type { FromSchema } from "~/index";

import { ajv } from "./ajv.util.test";
Expand Down Expand Up @@ -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 Set2 = FromSchema<
typeof setSchema,
{ omitAdditionalProperties: true }
>;

expectTypeOf<Set2>().toEqualTypeOf<{}>();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ended up using expect-type to assert against {} type.
Happy to find an alternative solution if we prefer not to introduce expect-type.

If we find expect-type valuable, we could refactor tests to use it extensively.

});
});
});

describe("Pattern properties", () => {
Expand Down Expand Up @@ -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<Set>().toEqualTypeOf<{}>();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same here: ended up using expect-type to assert against {} type.

});
});
});

describe("Unevaluated properties", () => {
Expand Down Expand Up @@ -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,
Expand All @@ -295,6 +319,25 @@ describe("Object schemas", () => {
};
expect(ajv.validate(addressSchema, addressInstance)).toBe(false);
});

describe("with omitAdditionalProperties option", () => {
it("rejects object with additional properties", () => {
type Address2 = FromSchema<
typeof addressSchema,
{ omitAdditionalProperties: true }
>;

const addressInstance2: Address2 = {
number: 13,
streetName: "Champs Elysées",
streetType: "Avenue",
direction: "NW",
// @ts-expect-error
additionalProperty: ["any", "value"],
};
expect(ajv.validate(addressSchema, addressInstance2)).toBe(true);
});
});
});

describe("Required + Typed additional properties", () => {
Expand Down Expand Up @@ -345,6 +388,24 @@ describe("Object schemas", () => {
};
expect(ajv.validate(addressSchema, addressInstance)).toBe(false);
});

describe("with omitAdditionalProperties option", () => {
it("rejects object with valid additional properties", () => {
type Address2 = FromSchema<
typeof addressSchema,
{ omitAdditionalProperties: true }
>;

const addressInstance2: Address2 = {
number: 13,
streetName: "Champs Elysées",
streetType: "Avenue",
// @ts-expect-error
additionalProperty: "additionalProperty",
};
expect(ajv.validate(addressSchema, addressInstance2)).toBe(true);
});
});
});

describe("Required missing in properties", () => {
Expand Down
26 changes: 26 additions & 0 deletions src/tests/readme/object.type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down