Skip to content

Commit

Permalink
feat: Add exact and stripUnknown method to object()
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense committed Dec 3, 2024
1 parent 87be159 commit adcdd8d
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 12 deletions.
29 changes: 20 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -360,7 +360,6 @@ let schema: ObjectSchema<Person> = object({
let badSchema: ObjectSchema<Person> = object({
name: number(),
});

```

### Extending built-in schema with new methods
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
4 changes: 3 additions & 1 deletion src/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export interface DateLocale {
}

export interface ObjectLocale {
noUnknown?: Message;
noUnknown?: Message<{ unknown: string[] }>;
exact?: Message<{ properties: string[] }>;
}

export interface ArrayLocale {
Expand Down Expand Up @@ -134,6 +135,7 @@ export let boolean: BooleanLocale = {

export let object: Required<ObjectLocale> = {
noUnknown: '${path} field has unspecified keys: ${unknown}',
exact: '${path} object contains unknown properties: ${properties}',
};

export let array: Required<ArrayLocale> = {
Expand Down
27 changes: 26 additions & 1 deletion src/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ export default class ObjectSchema<
});
}

clone(spec?: ObjectSchemaSpec): this {
clone(spec?: Partial<ObjectSchemaSpec>): this {
const next = super.clone(spec);
next.fields = { ...this.fields };
next._nodes = this._nodes;
Expand Down Expand Up @@ -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) {
Expand Down
12 changes: 12 additions & 0 deletions test/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down

0 comments on commit adcdd8d

Please sign in to comment.