diff --git a/README.md b/README.md index be5bea62..34c41622 100644 --- a/README.md +++ b/README.md @@ -265,20 +265,20 @@ let order = object({ skipAbsent: true, test(value, ctx) { if (!value.startsWith('s-')) { - return ctx.createError({ message: 'SKU missing correct prefix' }) + return ctx.createError({ message: 'SKU missing correct prefix' }); } if (!value.endsWith('-42a')) { - return ctx.createError({ message: 'SKU missing correct suffix' }) + return ctx.createError({ message: 'SKU missing correct suffix' }); } if (value.length < 10) { - return ctx.createError({ message: 'SKU is not the right length' }) + return ctx.createError({ message: 'SKU is not the right length' }); } - return true - } - }) -}) + return true; + }, + }), +}); -order.validate({ no: 1234, sku: 's-1a45-14a' }) +order.validate({ no: 1234, sku: 's-1a45-14a' }); ``` ### Composition and Reuse @@ -360,7 +360,6 @@ let schema: ObjectSchema = object({ let badSchema: ObjectSchema = object({ name: number(), }); - ``` ### Extending built-in schema with new methods @@ -1725,11 +1724,23 @@ let schema = object({ schema.cast({ prop: 5, other: 6 }); // => { myProp: 5, other: 6, Other: 6 } ``` +#### `object.exact(message?: string | function): Schema` + +Validates that the object does not contain extra or unknown properties + +#### `object.stripUnknown(): Schema` + +The same as `object().validate(value, { stripUnknown: true})`, but as a transform method. When set +any unknown properties will be removed. + #### `object.noUnknown(onlyKnownKeys: boolean = true, message?: string | function): Schema` Validate that the object value only contains keys specified in `shape`, pass `false` as the first argument to disable the check. Restricting keys to known, also enables `stripUnknown` option, when not in strict mode. +> Watch Out!: this method performs a transform and a validation, which may produce unexpected results. +> For more explicit behavior use `object().stripUnknown` and `object().exact()` + #### `object.camelCase(): Schema` Transforms all object keys to camelCase diff --git a/package.json b/package.json index 57ad9894..9c7b0d52 100644 --- a/package.json +++ b/package.json @@ -105,5 +105,6 @@ "tiny-case": "^1.0.3", "toposort": "^2.0.2", "type-fest": "^2.19.0" - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/locale.ts b/src/locale.ts index 6b70f68f..bc4e87a0 100644 --- a/src/locale.ts +++ b/src/locale.ts @@ -44,7 +44,8 @@ export interface DateLocale { } export interface ObjectLocale { - noUnknown?: Message; + noUnknown?: Message<{ unknown: string[] }>; + exact?: Message<{ properties: string[] }>; } export interface ArrayLocale { @@ -134,6 +135,7 @@ export let boolean: BooleanLocale = { export let object: Required = { noUnknown: '${path} field has unspecified keys: ${unknown}', + exact: '${path} object contains unknown properties: ${properties}', }; export let array: Required = { diff --git a/src/object.ts b/src/object.ts index 90898973..5825f686 100644 --- a/src/object.ts +++ b/src/object.ts @@ -282,7 +282,7 @@ export default class ObjectSchema< }); } - clone(spec?: ObjectSchemaSpec): this { + clone(spec?: Partial): this { const next = super.clone(spec); next.fields = { ...this.fields }; next._nodes = this._nodes; @@ -462,6 +462,31 @@ export default class ObjectSchema< return this.transform(parseJson); } + /** + * Similar to `noUnknown` but only validates that an object is the right shape without stripping the unknown keys + */ + exact(message?: Message): this { + return this.test({ + name: 'exact', + exclusive: true, + message: message || locale.exact, + test(value) { + if (value == null) return true; + + const unknownKeys = unknown(this.schema, value); + + return ( + unknownKeys.length === 0 || + this.createError({ params: { properties: unknownKeys.join(', ') } }) + ); + }, + }); + } + + stripUnknown(): this { + return this.clone({ noUnknown: true }); + } + noUnknown(message?: Message): this; noUnknown(noAllow: boolean, message?: Message): this; noUnknown(noAllow: Message | boolean = true, message = locale.noUnknown) { diff --git a/test/object.ts b/test/object.ts index 553c679e..22d3780f 100644 --- a/test/object.ts +++ b/test/object.ts @@ -427,6 +427,18 @@ describe('Object types', () => { }); }); + it('should work with exact', async () => { + let inst = object() + .shape({ + prop: mixed(), + }) + .exact(); + + await expect(inst.validate({ extra: 'field' })).rejects.toThrowError( + 'this object contains unknown properties: extra', + ); + }); + it('should strip specific fields', () => { let inst = object().shape({ prop: mixed().strip(false),