From 94b73c438b3d355253f488325e06c69378e71fc1 Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Wed, 29 Dec 2021 13:45:57 -0500 Subject: [PATCH] feat!: add json() method and remove default object/array coercion BREAKING CHANGE: object and array schema no longer parse JSON strings by default, nor do they return `null` for invalid casts. ```ts object().json().cast('{}') array().json().cast('[]') ``` to mimic the previous behavior --- README.md | 22 ++++++++++++++-------- src/array.ts | 24 +++++++----------------- src/object.ts | 18 ++++++------------ src/util/parseJson.ts | 18 ++++++++++++++++++ test/array.ts | 8 ++++---- test/object.ts | 8 +++++--- 6 files changed, 54 insertions(+), 44 deletions(-) create mode 100644 src/util/parseJson.ts diff --git a/README.md b/README.md index 3c1ae360c..8775af613 100644 --- a/README.md +++ b/README.md @@ -1420,24 +1420,28 @@ The default `cast` behavior for `array` is: [`JSON.parse`](https://developer.moz Failed casts return: `null`; -#### `array.of(type: Schema): Schema` +#### `array.of(type: Schema): this` Specify the schema of array elements. `of()` is optional and when omitted the array schema will not validate its contents. -#### `array.length(length: number | Ref, message?: string | function): Schema` +#### `array.json(): this` + +Attempt to parse input string values as JSON using [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse). + +#### `array.length(length: number | Ref, message?: string | function): this` Set a specific length requirement for the array. The `${length}` interpolation can be used in the `message` argument. -#### `array.min(limit: number | Ref, message?: string | function): Schema` +#### `array.min(limit: number | Ref, message?: string | function): this` Set a minimum length limit for the array. The `${min}` interpolation can be used in the `message` argument. -#### `array.max(limit: number | Ref, message?: string | function): Schema` +#### `array.max(limit: number | Ref, message?: string | function): this` Set a maximum length limit for the array. The `${max}` interpolation can be used in the `message` argument. -#### `array.ensure(): Schema` +#### `array.ensure(): this` Ensures that the value is an array, by setting the default to `[]` and transforming `null` and `undefined` values to an empty array as well. Any non-empty, non-array value will be wrapped in an array. @@ -1476,9 +1480,7 @@ yup.object({ }); ``` -The default `cast` behavior for `object` is: [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) - -Failed casts return: `null`; +object schema do not have any default transforms applied. #### Object schema defaults @@ -1546,6 +1548,10 @@ object({ }); ``` +#### `object.json(): this` + +Attempt to parse input string values as JSON using [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse). + #### `object.concat(schemaB: ObjectSchema): ObjectSchema` Creates a object schema, by applying all settings and fields from `schemaB` to the base, producing a new schema. diff --git a/src/array.ts b/src/array.ts index 07b445b78..d6cec856b 100644 --- a/src/array.ts +++ b/src/array.ts @@ -25,6 +25,7 @@ import { } from './util/types'; import Schema, { SchemaInnerTypeDescription, SchemaSpec } from './schema'; import { ResolveOptions } from './Condition'; +import parseJson from 'parse-json'; export type RejectorFn = ( value: any, @@ -55,24 +56,8 @@ export default class ArraySchema< }, }); - // `undefined` specifically means uninitialized, as opposed to - // "no subtype" + // `undefined` specifically means uninitialized, as opposed to "no subtype" this.innerType = type; - - this.withMutation(() => { - this.transform((values, _, ctx) => { - if (!ctx.spec.coarce) return values; - if (typeof values === 'string') { - try { - values = JSON.parse(values); - } catch (err) { - values = null; - } - } - - return ctx.isType(values) ? values : null; - }); - }); } private get _subType() { @@ -170,6 +155,11 @@ export default class ArraySchema< return next; } + /** Parse an input JSON string to an object */ + json() { + return this.transform(parseJson); + } + concat>( schema: ArraySchema, ): ArraySchema< diff --git a/src/object.ts b/src/object.ts index 46845a0a0..894c98f17 100644 --- a/src/object.ts +++ b/src/object.ts @@ -29,6 +29,7 @@ import type { PartialDeep, TypeFromShape, } from './util/objectTypes'; +import parseJson from './util/parseJson'; export type { AnyObject }; @@ -124,18 +125,6 @@ export default class ObjectSchema< }); this.withMutation(() => { - this.transform((value, _raw, ctx) => { - if (typeof value === 'string') { - try { - value = JSON.parse(value); - } catch (err) { - value = null; - } - } - if (ctx.isType(value)) return value; - return null; - }); - if (spec) { this.shape(spec as any); } @@ -442,6 +431,11 @@ export default class ObjectSchema< }); } + /** Parse an input JSON string to an object */ + json() { + return this.transform(parseJson); + } + noUnknown(message?: Message): this; noUnknown(noAllow: boolean, message?: Message): this; noUnknown(noAllow: Message | boolean = true, message = locale.noUnknown) { diff --git a/src/util/parseJson.ts b/src/util/parseJson.ts new file mode 100644 index 000000000..24f12ee1c --- /dev/null +++ b/src/util/parseJson.ts @@ -0,0 +1,18 @@ +import type { AnySchema } from '..'; +import type { TransformFunction } from '../types'; + +const parseJson: TransformFunction = (value, _, ctx: AnySchema) => { + if (typeof value !== 'string') { + return value; + } + + let parsed = value; + try { + parsed = JSON.parse(value); + } catch (err) { + /* */ + } + return ctx.isType(parsed) ? parsed : value; +}; + +export default parseJson; diff --git a/test/array.ts b/test/array.ts index fd5442695..d90588fc1 100644 --- a/test/array.ts +++ b/test/array.ts @@ -3,13 +3,13 @@ import { string, number, object, array, StringSchema, AnySchema } from '../src'; describe('Array types', () => { describe('casting', () => { it('should parse json strings', () => { - expect(array().cast('[2,3,5,6]')).toEqual([2, 3, 5, 6]); + expect(array().json().cast('[2,3,5,6]')).toEqual([2, 3, 5, 6]); }); - it('should return null for failed casts', () => { - expect(array().cast('asfasf', { assert: false })).toBeNull(); + it('should failed casts return input', () => { + expect(array().cast('asfasf', { assert: false })).toEqual('asfasf'); - expect(array().cast(null, { assert: false })).toBeNull(); + expect(array().cast('{}', { assert: false })).toEqual('{}'); }); it('should recursively cast fields', () => { diff --git a/test/object.ts b/test/object.ts index 3e516fd7a..3a2147f89 100644 --- a/test/object.ts +++ b/test/object.ts @@ -36,13 +36,15 @@ describe('Object types', () => { }); it('should parse json strings', () => { - expect(object({ hello: number() }).cast('{ "hello": "5" }')).toEqual({ + expect( + object({ hello: number() }).json().cast('{ "hello": "5" }'), + ).toEqual({ hello: 5, }); }); - it('should return null for failed casts', () => { - expect(object().cast('dfhdfh', { assert: false })).toBeNull(); + it('should return input for failed casts', () => { + expect(object().cast('dfhdfh', { assert: false })).toBe('dfhdfh'); }); it('should recursively cast fields', () => {