diff --git a/packages/schema-utils/src/index.ts b/packages/schema-utils/src/index.ts index 293e53b155..cd042ffde7 100644 --- a/packages/schema-utils/src/index.ts +++ b/packages/schema-utils/src/index.ts @@ -5,6 +5,7 @@ import { Mixin } from 'ts-mixer'; import { ArrayBufferBuilder, BufferBuilder, KeyofEnumBuilder, UintBuilder } from './custom-types'; import { InvalidParameter } from './errors'; +import { setDeepValue } from './utils'; class CustomTypeBuilder extends Mixin( JavaScriptTypeBuilder, @@ -69,6 +70,17 @@ export function Assert(schema: T, value: unknown): asserts va } else if (error.type === ValueErrorType.Union) { // Drill down into the union FindErrorInUnion(error); + } else if (error.type === ValueErrorType.Number && typeof error.value === 'string') { + // String instead of number, try to autocast + const currentValue = error.value; + const parsedNumber = Number(currentValue); + if (!Number.isNaN(parsedNumber) && currentValue === parsedNumber.toString()) { + // Autocast successful + const pathParts = error.path.slice(1).split('/'); + setDeepValue(value, pathParts, parsedNumber); + } else { + throw new InvalidParameter(error.message, error.path, error.type, error.value); + } } else { throw new InvalidParameter(error.message, error.path, error.type, error.value); } diff --git a/packages/schema-utils/src/utils.ts b/packages/schema-utils/src/utils.ts new file mode 100644 index 0000000000..28a52d0a0d --- /dev/null +++ b/packages/schema-utils/src/utils.ts @@ -0,0 +1,15 @@ +/** + * Sets a value in an object by a path + * From https://stackoverflow.com/a/53762921 + * @param obj object to set value in + * @param param path to the value + * @param value value to set + */ +export function setDeepValue(obj: any, [prop, ...path]: string[], value: any) { + if (!path.length) { + obj[prop] = value; + } else { + if (!(prop in obj)) obj[prop] = {}; + setDeepValue(obj[prop], path, value); + } +} diff --git a/packages/schema-utils/tests/number-autocast.test.ts b/packages/schema-utils/tests/number-autocast.test.ts new file mode 100644 index 0000000000..e90c5c73b4 --- /dev/null +++ b/packages/schema-utils/tests/number-autocast.test.ts @@ -0,0 +1,23 @@ +import { Type, Assert } from '../src'; + +describe('number-autocast', () => { + it('should string to number if needed', () => { + const schema = Type.Object({ + number: Type.Number(), + nested: Type.Object({ + number: Type.Number(), + }), + }); + + const input = { + number: '1', + nested: { + number: '1', + }, + }; + + Assert(schema, input); + expect(input.number).toEqual(1); + expect(input.nested.number).toEqual(1); + }); +}); diff --git a/packages/schema-utils/tests/utils.test.ts b/packages/schema-utils/tests/utils.test.ts new file mode 100644 index 0000000000..a45698e589 --- /dev/null +++ b/packages/schema-utils/tests/utils.test.ts @@ -0,0 +1,21 @@ +import { setDeepValue } from '../src/utils'; + +describe('setDeepValue', () => { + it('sets a deep value in an object', () => { + const obj = {}; + setDeepValue(obj, ['a', 'b', 'c'], 123); + expect(obj).toEqual({ a: { b: { c: 123 } } }); + }); + + it('overwrites existing values', () => { + const obj = { a: { b: { c: 123 } } }; + setDeepValue(obj, ['a', 'b', 'c'], 456); + expect(obj).toEqual({ a: { b: { c: 456 } } }); + }); + + it('creates intermediate objects if necessary', () => { + const obj = {}; + setDeepValue(obj, ['a', 'b', 'c'], 123); + expect(obj).toEqual({ a: { b: { c: 123 } } }); + }); +});