diff --git a/packages/kbn-config-schema/README.md b/packages/kbn-config-schema/README.md index e6f3e60128983..8719a2ae558ab 100644 --- a/packages/kbn-config-schema/README.md +++ b/packages/kbn-config-schema/README.md @@ -227,6 +227,9 @@ __Usage:__ const valueSchema = schema.arrayOf(schema.number()); ``` +__Notes:__ +* The `schema.arrayOf()` also supports a json string as input if it can be safely parsed using `JSON.parse` and if the resulting value is an array. + #### `schema.object()` Validates input data as an object with a predefined set of properties. @@ -249,6 +252,7 @@ const valueSchema = schema.object({ __Notes:__ * Using `allowUnknowns` is discouraged and should only be used in exceptional circumstances. Consider using `schema.recordOf()` instead. * Currently `schema.object()` always has a default value of `{}`, but this may change in the near future. Try to not rely on this behaviour and specify default value explicitly or use `schema.maybe()` if the value is optional. +* `schema.object()` also supports a json string as input if it can be safely parsed using `JSON.parse` and if the resulting value is a plain object. #### `schema.recordOf()` @@ -267,6 +271,7 @@ const valueSchema = schema.recordOf(schema.string(), schema.number()); __Notes:__ * You can use a union of literal types as a record's key schema to restrict record to a specific set of keys, e.g. `schema.oneOf([schema.literal('isEnabled'), schema.literal('name')])`. +* `schema.recordOf()` also supports a json string as input if it can be safely parsed using `JSON.parse` and if the resulting value is a plain object. #### `schema.mapOf()` @@ -283,6 +288,10 @@ __Usage:__ const valueSchema = schema.mapOf(schema.string(), schema.number()); ``` +__Notes:__ +* You can use a union of literal types as a record's key schema to restrict record to a specific set of keys, e.g. `schema.oneOf([schema.literal('isEnabled'), schema.literal('name')])`. +* `schema.mapOf()` also supports a json string as input if it can be safely parsed using `JSON.parse` and if the resulting value is a plain object. + ### Advanced types #### `schema.oneOf()` diff --git a/packages/kbn-config-schema/src/internals/index.ts b/packages/kbn-config-schema/src/internals/index.ts index 044c3050f9fa8..8f5d09e5b8b49 100644 --- a/packages/kbn-config-schema/src/internals/index.ts +++ b/packages/kbn-config-schema/src/internals/index.ts @@ -250,12 +250,23 @@ export const internals = Joi.extend([ base: Joi.object(), coerce(value: any, state: State, options: ValidationOptions) { - // If value isn't defined, let Joi handle default value if it's defined. - if (value !== undefined && !isPlainObject(value)) { - return this.createError('object.base', { value }, state, options); + if (value === undefined || isPlainObject(value)) { + return value; } - return value; + if (options.convert && typeof value === 'string') { + try { + const parsed = JSON.parse(value); + if (isPlainObject(parsed)) { + return parsed; + } + return this.createError('object.base', { value: parsed }, state, options); + } catch (e) { + return this.createError('object.parse', { value }, state, options); + } + } + + return this.createError('object.base', { value }, state, options); }, rules: [anyCustomRule], }, @@ -263,9 +274,23 @@ export const internals = Joi.extend([ name: 'map', coerce(value: any, state: State, options: ValidationOptions) { + if (value === undefined) { + return value; + } if (isPlainObject(value)) { return new Map(Object.entries(value)); } + if (options.convert && typeof value === 'string') { + try { + const parsed = JSON.parse(value); + if (isPlainObject(parsed)) { + return new Map(Object.entries(parsed)); + } + return this.createError('map.base', { value: parsed }, state, options); + } catch (e) { + return this.createError('map.parse', { value }, state, options); + } + } return value; }, @@ -321,11 +346,23 @@ export const internals = Joi.extend([ { name: 'record', pre(value: any, state: State, options: ValidationOptions) { - if (!isPlainObject(value)) { - return this.createError('record.base', { value }, state, options); + if (value === undefined || isPlainObject(value)) { + return value; } - return value as any; + if (options.convert && typeof value === 'string') { + try { + const parsed = JSON.parse(value); + if (isPlainObject(parsed)) { + return parsed; + } + return this.createError('record.base', { value: parsed }, state, options); + } catch (e) { + return this.createError('record.parse', { value }, state, options); + } + } + + return this.createError('record.base', { value }, state, options); }, rules: [ anyCustomRule, @@ -371,12 +408,23 @@ export const internals = Joi.extend([ base: Joi.array(), coerce(value: any, state: State, options: ValidationOptions) { - // If value isn't defined, let Joi handle default value if it's defined. - if (value !== undefined && !Array.isArray(value)) { - return this.createError('array.base', { value }, state, options); + if (value === undefined || Array.isArray(value)) { + return value; } - return value; + if (options.convert && typeof value === 'string') { + try { + const parsed = JSON.parse(value); + if (Array.isArray(parsed)) { + return parsed; + } + return this.createError('array.base', { value: parsed }, state, options); + } catch (e) { + return this.createError('array.parse', { value }, state, options); + } + } + + return this.createError('array.base', { value }, state, options); }, rules: [anyCustomRule], }, diff --git a/packages/kbn-config-schema/src/types/__snapshots__/array_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/array_type.test.ts.snap deleted file mode 100644 index 685b13c00587e..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/array_type.test.ts.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`#maxSize returns error when more items 1`] = `"array size is [2], but cannot be greater than [1]"`; - -exports[`#minSize returns error when fewer items 1`] = `"array size is [1], but cannot be smaller than [2]"`; - -exports[`fails for null values if optional 1`] = `"[0]: expected value of type [string] but got [null]"`; - -exports[`fails if mixed types of content in array 1`] = `"[2]: expected value of type [string] but got [boolean]"`; - -exports[`fails if wrong input type 1`] = `"expected value of type [array] but got [string]"`; - -exports[`fails if wrong type of content in array 1`] = `"[0]: expected value of type [string] but got [number]"`; - -exports[`includes namespace in failure when wrong item type 1`] = `"[foo-namespace.0]: expected value of type [string] but got [number]"`; - -exports[`includes namespace in failure when wrong top-level type 1`] = `"[foo-namespace]: expected value of type [array] but got [string]"`; - -exports[`object within array with required 1`] = `"[0.foo]: expected value of type [string] but got [undefined]"`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/map_of_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/map_of_type.test.ts.snap deleted file mode 100644 index 21b71ddd2487d..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/map_of_type.test.ts.snap +++ /dev/null @@ -1,11 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`fails when not receiving expected key type 1`] = `"[key(\\"name\\")]: expected value of type [number] but got [string]"`; - -exports[`fails when not receiving expected value type 1`] = `"[name]: expected value of type [string] but got [number]"`; - -exports[`includes namespace in failure when wrong key type 1`] = `"[foo-namespace.key(\\"name\\")]: expected value of type [number] but got [string]"`; - -exports[`includes namespace in failure when wrong top-level type 1`] = `"[foo-namespace]: expected value of type [Map] or [object] but got [Array]"`; - -exports[`includes namespace in failure when wrong value type 1`] = `"[foo-namespace.name]: expected value of type [string] but got [number]"`; diff --git a/packages/kbn-config-schema/src/types/__snapshots__/object_type.test.ts.snap b/packages/kbn-config-schema/src/types/__snapshots__/object_type.test.ts.snap deleted file mode 100644 index c5e47ac09f034..0000000000000 --- a/packages/kbn-config-schema/src/types/__snapshots__/object_type.test.ts.snap +++ /dev/null @@ -1,24 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`allowUnknowns = true affects only own keys 1`] = `"[foo.baz]: definition for this key is missing"`; - -exports[`called with wrong type 1`] = `"expected a plain object value, but found [string] instead."`; - -exports[`called with wrong type 2`] = `"expected a plain object value, but found [number] instead."`; - -exports[`does not allow unknown keys when allowUnknowns = false 1`] = `"[bar]: definition for this key is missing"`; - -exports[`fails if key does not exist in schema 1`] = `"[bar]: definition for this key is missing"`; - -exports[`fails if missing required value 1`] = `"[name]: expected value of type [string] but got [undefined]"`; - -exports[`handles oneOf 1`] = ` -"[key]: types that failed validation: -- [key.0]: expected value of type [string] but got [number]" -`; - -exports[`includes namespace in failure when wrong top-level type 1`] = `"[foo-namespace]: expected a plain object value, but found [Array] instead."`; - -exports[`includes namespace in failure when wrong value type 1`] = `"[foo-namespace.foo]: expected value of type [string] but got [number]"`; - -exports[`object within object with required 1`] = `"[foo.bar]: expected value of type [string] but got [undefined]"`; diff --git a/packages/kbn-config-schema/src/types/array_type.test.ts b/packages/kbn-config-schema/src/types/array_type.test.ts index c6943e0d1b5f3..73661ef849cf4 100644 --- a/packages/kbn-config-schema/src/types/array_type.test.ts +++ b/packages/kbn-config-schema/src/types/array_type.test.ts @@ -24,29 +24,65 @@ test('returns value if it matches the type', () => { expect(type.validate(['foo', 'bar', 'baz'])).toEqual(['foo', 'bar', 'baz']); }); +test('properly parse the value if input is a string', () => { + const type = schema.arrayOf(schema.string()); + expect(type.validate('["foo", "bar", "baz"]')).toEqual(['foo', 'bar', 'baz']); +}); + test('fails if wrong input type', () => { const type = schema.arrayOf(schema.string()); - expect(() => type.validate('test')).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(12)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [array] but got [number]"` + ); +}); + +test('fails if string input cannot be parsed', () => { + const type = schema.arrayOf(schema.string()); + expect(() => type.validate('test')).toThrowErrorMatchingInlineSnapshot( + `"could not parse array value from [test]"` + ); +}); + +test('fails with correct type if parsed input is not an array', () => { + const type = schema.arrayOf(schema.string()); + expect(() => type.validate('{"foo": "bar"}')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [array] but got [Object]"` + ); }); test('includes namespace in failure when wrong top-level type', () => { const type = schema.arrayOf(schema.string()); - expect(() => type.validate('test', {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); + expect(() => type.validate('test', {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace]: could not parse array value from [test]"` + ); }); test('includes namespace in failure when wrong item type', () => { const type = schema.arrayOf(schema.string()); - expect(() => type.validate([123], {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); + expect(() => type.validate([123], {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace.0]: expected value of type [string] but got [number]"` + ); }); test('fails if wrong type of content in array', () => { const type = schema.arrayOf(schema.string()); - expect(() => type.validate([1, 2, 3])).toThrowErrorMatchingSnapshot(); + expect(() => type.validate([1, 2, 3])).toThrowErrorMatchingInlineSnapshot( + `"[0]: expected value of type [string] but got [number]"` + ); +}); + +test('fails when parsing if wrong type of content in array', () => { + const type = schema.arrayOf(schema.string()); + expect(() => type.validate('[1, 2, 3]')).toThrowErrorMatchingInlineSnapshot( + `"[0]: expected value of type [string] but got [number]"` + ); }); test('fails if mixed types of content in array', () => { const type = schema.arrayOf(schema.string()); - expect(() => type.validate(['foo', 'bar', true, {}])).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(['foo', 'bar', true, {}])).toThrowErrorMatchingInlineSnapshot( + `"[2]: expected value of type [string] but got [boolean]"` + ); }); test('returns empty array if input is empty but type has default value', () => { @@ -61,7 +97,9 @@ test('returns empty array if input is empty even if type is required', () => { test('fails for null values if optional', () => { const type = schema.arrayOf(schema.maybe(schema.string())); - expect(() => type.validate([null])).toThrowErrorMatchingSnapshot(); + expect(() => type.validate([null])).toThrowErrorMatchingInlineSnapshot( + `"[0]: expected value of type [string] but got [null]"` + ); }); test('handles default values for undefined values', () => { @@ -108,7 +146,9 @@ test('object within array with required', () => { const value = [{}]; - expect(() => type.validate(value)).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[0.foo]: expected value of type [string] but got [undefined]"` + ); }); describe('#minSize', () => { @@ -119,7 +159,7 @@ describe('#minSize', () => { test('returns error when fewer items', () => { expect(() => schema.arrayOf(schema.string(), { minSize: 2 }).validate(['foo']) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot(`"array size is [1], but cannot be smaller than [2]"`); }); }); @@ -131,6 +171,6 @@ describe('#maxSize', () => { test('returns error when more items', () => { expect(() => schema.arrayOf(schema.string(), { maxSize: 1 }).validate(['foo', 'bar']) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot(`"array size is [2], but cannot be greater than [1]"`); }); }); diff --git a/packages/kbn-config-schema/src/types/array_type.ts b/packages/kbn-config-schema/src/types/array_type.ts index 73f2d0e614056..ad74f375588ad 100644 --- a/packages/kbn-config-schema/src/types/array_type.ts +++ b/packages/kbn-config-schema/src/types/array_type.ts @@ -49,6 +49,8 @@ export class ArrayType extends Type { case 'any.required': case 'array.base': return `expected value of type [array] but got [${typeDetect(value)}]`; + case 'array.parse': + return `could not parse array value from [${value}]`; case 'array.min': return `array size is [${value.length}], but cannot be smaller than [${limit}]`; case 'array.max': diff --git a/packages/kbn-config-schema/src/types/map_of_type.test.ts b/packages/kbn-config-schema/src/types/map_of_type.test.ts index 6b9b700efdc3c..3cb3d2d0b6862 100644 --- a/packages/kbn-config-schema/src/types/map_of_type.test.ts +++ b/packages/kbn-config-schema/src/types/map_of_type.test.ts @@ -29,13 +29,46 @@ test('handles object as input', () => { expect(type.validate(value)).toEqual(expected); }); +test('properly parse the value if input is a string', () => { + const type = schema.mapOf(schema.string(), schema.string()); + const value = `{"name": "foo"}`; + const expected = new Map([['name', 'foo']]); + + expect(type.validate(value)).toEqual(expected); +}); + +test('fails if string input cannot be parsed', () => { + const type = schema.mapOf(schema.string(), schema.string()); + expect(() => type.validate(`invalidjson`)).toThrowErrorMatchingInlineSnapshot( + `"could not parse map value from [invalidjson]"` + ); +}); + +test('fails with correct type if parsed input is not an object', () => { + const type = schema.mapOf(schema.string(), schema.string()); + expect(() => type.validate('[1,2,3]')).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [Map] or [object] but got [Array]"` + ); +}); + test('fails when not receiving expected value type', () => { const type = schema.mapOf(schema.string(), schema.string()); const value = { name: 123, }; - expect(() => type.validate(value)).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[name]: expected value of type [string] but got [number]"` + ); +}); + +test('fails after parsing when not receiving expected value type', () => { + const type = schema.mapOf(schema.string(), schema.string()); + const value = `{"name": 123}`; + + expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[name]: expected value of type [string] but got [number]"` + ); }); test('fails when not receiving expected key type', () => { @@ -44,12 +77,25 @@ test('fails when not receiving expected key type', () => { name: 'foo', }; - expect(() => type.validate(value)).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[key(\\"name\\")]: expected value of type [number] but got [string]"` + ); +}); + +test('fails after parsing when not receiving expected key type', () => { + const type = schema.mapOf(schema.number(), schema.string()); + const value = `{"name": "foo"}`; + + expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[key(\\"name\\")]: expected value of type [number] but got [string]"` + ); }); test('includes namespace in failure when wrong top-level type', () => { const type = schema.mapOf(schema.string(), schema.string()); - expect(() => type.validate([], {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); + expect(() => type.validate([], {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace]: expected value of type [Map] or [object] but got [Array]"` + ); }); test('includes namespace in failure when wrong value type', () => { @@ -58,7 +104,9 @@ test('includes namespace in failure when wrong value type', () => { name: 123, }; - expect(() => type.validate(value, {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(value, {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace.name]: expected value of type [string] but got [number]"` + ); }); test('includes namespace in failure when wrong key type', () => { @@ -67,7 +115,9 @@ test('includes namespace in failure when wrong key type', () => { name: 'foo', }; - expect(() => type.validate(value, {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(value, {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace.key(\\"name\\")]: expected value of type [number] but got [string]"` + ); }); test('returns default value if undefined', () => { diff --git a/packages/kbn-config-schema/src/types/map_type.ts b/packages/kbn-config-schema/src/types/map_type.ts index c637eccb79571..1c0c473f98ec1 100644 --- a/packages/kbn-config-schema/src/types/map_type.ts +++ b/packages/kbn-config-schema/src/types/map_type.ts @@ -48,6 +48,8 @@ export class MapOfType extends Type> { case 'any.required': case 'map.base': return `expected value of type [Map] or [object] but got [${typeDetect(value)}]`; + case 'map.parse': + return `could not parse map value from [${value}]`; case 'map.key': case 'map.value': const childPathWithIndex = path.slice(); diff --git a/packages/kbn-config-schema/src/types/object_type.test.ts b/packages/kbn-config-schema/src/types/object_type.test.ts index 41bba1a78d478..5786984cf7ebd 100644 --- a/packages/kbn-config-schema/src/types/object_type.test.ts +++ b/packages/kbn-config-schema/src/types/object_type.test.ts @@ -30,13 +30,42 @@ test('returns value by default', () => { expect(type.validate(value)).toEqual({ name: 'test' }); }); +test('properly parse the value if input is a string', () => { + const type = schema.object({ + name: schema.string(), + }); + const value = `{"name": "test"}`; + + expect(type.validate(value)).toEqual({ name: 'test' }); +}); + +test('fails if string input cannot be parsed', () => { + const type = schema.object({ + name: schema.string(), + }); + expect(() => type.validate(`invalidjson`)).toThrowErrorMatchingInlineSnapshot( + `"could not parse object value from [invalidjson]"` + ); +}); + +test('fails with correct type if parsed input is not an object', () => { + const type = schema.object({ + name: schema.string(), + }); + expect(() => type.validate('[1,2,3]')).toThrowErrorMatchingInlineSnapshot( + `"expected a plain object value, but found [Array] instead."` + ); +}); + test('fails if missing required value', () => { const type = schema.object({ name: schema.string(), }); const value = {}; - expect(() => type.validate(value)).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[name]: expected value of type [string] but got [undefined]"` + ); }); test('returns value if undefined string with default', () => { @@ -57,7 +86,9 @@ test('fails if key does not exist in schema', () => { foo: 'bar', }; - expect(() => type.validate(value)).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[bar]: definition for this key is missing"` + ); }); test('defined object within object', () => { @@ -96,7 +127,9 @@ test('object within object with required', () => { }); const value = { foo: {} }; - expect(() => type.validate(value)).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[foo.bar]: expected value of type [string] but got [undefined]"` + ); }); describe('#validate', () => { @@ -127,8 +160,12 @@ describe('#validate', () => { test('called with wrong type', () => { const type = schema.object({}); - expect(() => type.validate('foo')).toThrowErrorMatchingSnapshot(); - expect(() => type.validate(123)).toThrowErrorMatchingSnapshot(); + expect(() => type.validate('foo')).toThrowErrorMatchingInlineSnapshot( + `"could not parse object value from [foo]"` + ); + expect(() => type.validate(123)).toThrowErrorMatchingInlineSnapshot( + `"expected a plain object value, but found [number] instead."` + ); }); test('handles oneOf', () => { @@ -137,7 +174,10 @@ test('handles oneOf', () => { }); expect(type.validate({ key: 'foo' })).toEqual({ key: 'foo' }); - expect(() => type.validate({ key: 123 })).toThrowErrorMatchingSnapshot(); + expect(() => type.validate({ key: 123 })).toThrowErrorMatchingInlineSnapshot(` +"[key]: types that failed validation: +- [key.0]: expected value of type [string] but got [number]" +`); }); test('handles references', () => { @@ -186,7 +226,9 @@ test('includes namespace in failure when wrong top-level type', () => { foo: schema.string(), }); - expect(() => type.validate([], {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); + expect(() => type.validate([], {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace]: expected a plain object value, but found [Array] instead."` + ); }); test('includes namespace in failure when wrong value type', () => { @@ -197,7 +239,9 @@ test('includes namespace in failure when wrong value type', () => { foo: 123, }; - expect(() => type.validate(value, {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); + expect(() => type.validate(value, {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot( + `"[foo-namespace.foo]: expected value of type [string] but got [number]"` + ); }); test('individual keys can validated', () => { @@ -241,7 +285,7 @@ test('allowUnknowns = true affects only own keys', () => { baz: 'baz', }, }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot(`"[foo.baz]: definition for this key is missing"`); }); test('does not allow unknown keys when allowUnknowns = false', () => { @@ -253,5 +297,5 @@ test('does not allow unknown keys when allowUnknowns = false', () => { type.validate({ bar: 'baz', }) - ).toThrowErrorMatchingSnapshot(); + ).toThrowErrorMatchingInlineSnapshot(`"[bar]: definition for this key is missing"`); }); diff --git a/packages/kbn-config-schema/src/types/object_type.ts b/packages/kbn-config-schema/src/types/object_type.ts index 986448481cd83..d2e6c708c263c 100644 --- a/packages/kbn-config-schema/src/types/object_type.ts +++ b/packages/kbn-config-schema/src/types/object_type.ts @@ -61,6 +61,8 @@ export class ObjectType

extends Type> case 'any.required': case 'object.base': return `expected a plain object value, but found [${typeDetect(value)}] instead.`; + case 'object.parse': + return `could not parse object value from [${value}]`; case 'object.allowUnknown': return `definition for this key is missing`; case 'object.child': diff --git a/packages/kbn-config-schema/src/types/one_of_type.test.ts b/packages/kbn-config-schema/src/types/one_of_type.test.ts index c84ae49df7aef..c9da1a6cd8494 100644 --- a/packages/kbn-config-schema/src/types/one_of_type.test.ts +++ b/packages/kbn-config-schema/src/types/one_of_type.test.ts @@ -138,7 +138,7 @@ test('fails if nested union type fail', () => { - [0]: expected value of type [boolean] but got [string] - [1]: types that failed validation: - [0]: types that failed validation: - - [0]: expected a plain object value, but found [string] instead. + - [0]: could not parse object value from [aaa] - [1]: expected value of type [number] but got [string]" `); }); diff --git a/packages/kbn-config-schema/src/types/record_of_type.test.ts b/packages/kbn-config-schema/src/types/record_of_type.test.ts index 2172160e8d181..f3ab1925597b5 100644 --- a/packages/kbn-config-schema/src/types/record_of_type.test.ts +++ b/packages/kbn-config-schema/src/types/record_of_type.test.ts @@ -27,6 +27,20 @@ test('handles object as input', () => { expect(type.validate(value)).toEqual({ name: 'foo' }); }); +test('properly parse the value if input is a string', () => { + const type = schema.recordOf(schema.string(), schema.string()); + const value = `{"name": "foo"}`; + expect(type.validate(value)).toEqual({ name: 'foo' }); +}); + +test('fails with correct type if parsed input is a plain object', () => { + const type = schema.recordOf(schema.string(), schema.string()); + const value = `["a", "b"]`; + expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"expected value of type [object] but got [Array]"` + ); +}); + test('fails when not receiving expected value type', () => { const type = schema.recordOf(schema.string(), schema.string()); const value = { @@ -38,6 +52,15 @@ test('fails when not receiving expected value type', () => { ); }); +test('fails after parsing when not receiving expected value type', () => { + const type = schema.recordOf(schema.string(), schema.string()); + const value = `{"name": 123}`; + + expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot( + `"[name]: expected value of type [string] but got [number]"` + ); +}); + test('fails when not receiving expected key type', () => { const type = schema.recordOf( schema.oneOf([schema.literal('nickName'), schema.literal('lastName')]), @@ -55,6 +78,21 @@ test('fails when not receiving expected key type', () => { `); }); +test('fails after parsing when not receiving expected key type', () => { + const type = schema.recordOf( + schema.oneOf([schema.literal('nickName'), schema.literal('lastName')]), + schema.string() + ); + + const value = `{"name": "foo"}`; + + expect(() => type.validate(value)).toThrowErrorMatchingInlineSnapshot(` +"[key(\\"name\\")]: types that failed validation: +- [0]: expected value to equal [nickName] but got [name] +- [1]: expected value to equal [lastName] but got [name]" +`); +}); + test('includes namespace in failure when wrong top-level type', () => { const type = schema.recordOf(schema.string(), schema.string()); expect(() => type.validate([], {}, 'foo-namespace')).toThrowErrorMatchingInlineSnapshot( diff --git a/packages/kbn-config-schema/src/types/record_type.ts b/packages/kbn-config-schema/src/types/record_type.ts index 82e585f685c56..b795c83acdadb 100644 --- a/packages/kbn-config-schema/src/types/record_type.ts +++ b/packages/kbn-config-schema/src/types/record_type.ts @@ -40,6 +40,8 @@ export class RecordOfType extends Type> { case 'any.required': case 'record.base': return `expected value of type [object] but got [${typeDetect(value)}]`; + case 'record.parse': + return `could not parse record value from [${value}]`; case 'record.key': case 'record.value': const childPathWithIndex = path.slice(); diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts index a305f85650b9c..646ea168b52a5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -150,7 +150,7 @@ describe('params validation', () => { expect(() => { validateParams(actionType, { documents: ['should be an object'] }); }).toThrowErrorMatchingInlineSnapshot( - `"error validating action params: [documents.0]: expected value of type [object] but got [string]"` + `"error validating action params: [documents.0]: could not parse record value from [should be an object]"` ); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index ae1d8c3fddc8b..39da5db5c1c27 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -130,7 +130,7 @@ describe('config validation', () => { validateConfig(actionType, config); }).toThrowErrorMatchingInlineSnapshot(` "error validating action type config: [headers]: types that failed validation: -- [headers.0]: expected value of type [object] but got [string] +- [headers.0]: could not parse record value from [application/json] - [headers.1]: expected value to equal [null] but got [application/json]" `); }); diff --git a/x-pack/plugins/spaces/server/lib/space_schema.test.ts b/x-pack/plugins/spaces/server/lib/space_schema.test.ts index 92ccb5401893a..6330fcef19e8d 100644 --- a/x-pack/plugins/spaces/server/lib/space_schema.test.ts +++ b/x-pack/plugins/spaces/server/lib/space_schema.test.ts @@ -93,7 +93,7 @@ describe('#disabledFeatures', () => { disabledFeatures: 'foo', }) ).toThrowErrorMatchingInlineSnapshot( - `"[disabledFeatures]: expected value of type [array] but got [string]"` + `"[disabledFeatures]: could not parse array value from [foo]"` ); }); diff --git a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js index be2af7cb76fd5..be6139ed7a0a7 100644 --- a/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js +++ b/x-pack/test/api_integration/apis/management/rollup/index_patterns_extensions.js @@ -79,7 +79,7 @@ export default function({ getService }) { uri = `${BASE_URI}?${querystring.stringify(params)}`; ({ body } = await supertest.get(uri).expect(400)); expect(body.message).to.contain( - '[request query.meta_fields]: expected value of type [array]' + '[request query.meta_fields]: could not parse array value from [stringValue]' ); });