Repository: modiimedia/arri (language: rust)

Issue:
[ts-schema] feature: compiled validators should also compile a `coerce` function
**Describe the feature**

We should also compile a highly optimized version of `a.coerce()`, when calling `a.compile()`. This will improve performance of RPCs that use `GET` HTTP methods, since the `coerce()` function is called to coerce url query params into the target type.

**Additional context**

<!-- Add any other context or screenshots about the feature request here. -->

**Would you be willing to implement this feature?**

Yes


Discussion / hints:

Fix patch:
diff --git a/languages/ts/ts-schema/benchmark/src/objects.ts b/languages/ts/ts-schema/benchmark/src/objects.ts
index 44f906f3..dd79dd34 100644
--- a/languages/ts/ts-schema/benchmark/src/objects.ts
+++ b/languages/ts/ts-schema/benchmark/src/objects.ts
@@ -616,6 +616,9 @@ void benny.suite(
     benny.add('Arri', () => {
         a.coerce(ArriUser, inputWithStringKeys);
     }),
+    benny.add('Arri (Compiled)', () => {
+        $$ArriUser.coerce(inputWithStringKeys);
+    }),
     benny.add('TypeBox', () => {
         Value.Convert(TypeBoxUser, inputWithStringKeys);
     }),
diff --git a/languages/ts/ts-schema/src/compile.ts b/languages/ts/ts-schema/src/compile.ts
index 2966abca..3d5099fe 100644
--- a/languages/ts/ts-schema/src/compile.ts
+++ b/languages/ts/ts-schema/src/compile.ts
@@ -1,4 +1,4 @@
-import { isSchemaFormEnum, isSchemaFormType } from '@arrirpc/type-defs';
+import { isSchemaFormEnum, isSchemaFormType, Schema } from '@arrirpc/type-defs';
 import { StandardSchemaV1 } from '@standard-schema/spec';
 
 import { createStandardSchemaProperty } from './adapters';
@@ -13,12 +13,16 @@ import {
     int16Min,
     int32Max,
     int32Min,
+    int64Max,
+    int64Min,
     uint8Max,
     uint8Min,
     uint16Max,
     uint16Min,
     uint32Max,
     uint32Min,
+    uint64Max,
+    uint64Min,
 } from './lib/numberConstants';
 import {
     type ASchema,
@@ -42,13 +46,21 @@ export interface CompiledValidator<TSchema extends ASchema<any>> {
      */
     validate: (input: unknown) => input is InferType<TSchema>;
     /**
-     * Parse a JSON string or the result of JSON.parse(). Throws an error if parsing fails.
+     * Parse a JSON string or the result of JSON.parse(). Returns a Result<T>.
      */
     parse: (input: unknown) => Result<InferType<TSchema>>;
     /**
      * Parse a JSON string or the result of JSON.parse(). Throws an error if parsing fails.
      */
     parseUnsafe: (input: unknown) => InferType<TSchema>;
+    /**
+     * Coerce an object into <T>. Returns Result<T>.
+     */
+    coerce: (input: unknown) => Result<InferType<TSchema>>;
+    /**
+     * Coerce an object into <T>. Throws an error if coercion fails.
+     */
+    coerceUnsafe: (input: unknown) => InferType<TSchema>;
     /**
      * Serialize to JSON
      */
@@ -62,6 +74,7 @@ export interface CompiledValidator<TSchema extends ASchema<any>> {
      */
     compiledCode: {
         parse: string;
+        coerce: string;
         serialize: string;
         validate: string;
     };
@@ -77,8 +90,10 @@ export function compile<TSchema extends ASchema<any>>(
     schema: TSchema,
 ): CompiledValidatorWithAdapters<TSchema> {
     const validateCode = getSchemaValidationCode('input', schema);
-    const parser = getCompiledParser('input', schema);
+    const parser = getCompiledParser('input', schema, false);
     const parserFn = parser.fn;
+    const coercer = getCompiledParser('input', schema, true);
+    const coercerFn = coercer.fn;
     const serializer = getCompiledSerializer(schema);
     const serializeFn = serializer.fn;
     const serialize = (input: InferType<TSchema>): Result<string> => {
@@ -135,6 +150,38 @@ export function compile<TSchema extends ASchema<any>>(
             };
         }
     };
+    const coerce = (input: unknown): Result<InferType<TSchema>> => {
+        const context = newValidationContext();
+        try {
+            const result = coercerFn(input, context);
+            if (context.errors.length) {
+                return {
+                    success: false,
+                    errors: context.errors,
+                };
+            }
+            return {
+                success: true,
+                value: result,
+            };
+        } catch (err) {
+            if (context.errors.length) {
+                return {
+                    success: false,
+                    errors: context.errors,
+                };
+            }
+            return {
+                success: false,
+                errors: [
+                    {
+                        message: err instanceof Error ? err.message : `${err}`,
+                        instancePath: '/',
+                    },
+                ],
+            };
+        }
+    };
     const result: CompiledValidatorWithAdapters<TSchema> = {
         schema: JSON.parse(JSON.stringify(schema)),
         validate,
@@ -147,6 +194,15 @@ export function compile<TSchema extends ASchema<any>>(
             }
             return result!;
         },
+        coerce: coerce,
+        coerceUnsafe(input) {
+            const context = newValidationContext();
+            const result = coercerFn(input, context);
+            if (context.errors.length) {
+                throw new ValidationException({ errors: context.errors });
+            }
+            return result!;
+        },
         serialize,
         serializeUnsafe(input) {
             return serializeFn(input);
@@ -154,6 +210,7 @@ export function compile<TSchema extends ASchema<any>>(
         compiledCode: {
             validate: validateCode,
             parse: parser.code,
+            coerce: coercer.code,
             serialize: serializer.code,
         },
         '~standard': createStandardSchemaProperty(validate, (input, ctx) => {
@@ -174,9 +231,10 @@ type CompiledParser<TSchema extends ASchema<any>> = SchemaValidator<
 
 export function getCompiledParser<TSchema extends ASchema<any>>(
     input: string,
-    schema: TSchema,
+    schema: Schema,
+    shouldCoerce: boolean,
 ): { fn: CompiledParser<TSchema>; code: string } {
-    const code = getSchemaDecodingCode(input, schema);
+    const code = getSchemaDecodingCode(input, schema, shouldCoerce);
     if (isSchemaFormType(schema)) {
         switch (schema.type) {
             case 'float32':
@@ -336,6 +394,69 @@ export function getCompiledParser<TSchema extends ASchema<any>>(
                     code,
                 };
             case 'boolean':
+                if (shouldCoerce) {
+                    if (schema.nullable) {
+                        return {
+                            code,
+                            fn(input, context) {
+                                switch (input) {
+                                    case true:
+                                        return true;
+                                    case false:
+                                        return false;
+                                    case 'true':
+                                    case 'TRUE':
+                                    case '1':
+                                    case 1:
+                                        return true;
+                                    case 'false':
+                                    case 'FALSE':
+                                    case '0':
+                                    case 0:
+                                        return false;
+                                    case null:
+                                    case 'null':
+                                        return null;
+                                    default:
+                                        context.errors.push({
+                                            instancePath: '/',
+                                            schemaPath: '/type',
+                                            message: `Unable to coerce ${input} into boolean`,
+                                        });
+                                        return undefined;
+                                }
+                            },
+                        };
+                    }
+                    return {
+                        code,
+                        fn(input, context) {
+                            switch (input) {
+                                case true:
+                                    return true;
+                                case false:
+                                    return false;
+                                case 'true':
+                                case 'TRUE':
+                                case '1':
+                                case 1:
+                                    return true;
+                                case 'false':
+                                case 'FALSE':
+                                case '0':
+                                case 0:
+                                    return false;
+                                default:
+                                    context.errors.push({
+                                        instancePath: '/',
+                                        schemaPath: '/type',
+                                        message: `Unable to coerce ${input} into boolean`,
+                                    });
+                                    return undefined;
+                            }
+                        },
+                    };
+                }
                 if (schema.nullable) {
                     return {
                         code,
@@ -426,6 +547,78 @@ export function getCompiledParser<TSchema extends ASchema<any>>(
                     code,
                 };
             case 'timestamp':
+                if (shouldCoerce) {
+                    if (schema.nullable) {
+                        return {
+                            fn(input, context) {
+                                if (typeof input === 'string') {
+                                    const decodedInput = new Date(input);
+                                    if (
+                                        !Number.isNaN(decodedInput.getMonth())
+                                    ) {
+                                        return decodedInput;
+                                    }
+                                    if (input === 'null') {
+                                        return null;
+                                    }
+                                }
+                                if (typeof input === 'number') {
+                                    const decodedInput = new Date(input);
+                                    if (
+                                        !Number.isNaN(decodedInput.getMonth())
+                                    ) {
+                                        return decodedInput;
+                                    }
+                                }
+                                if (
+                                    typeof input === 'object' &&
+                                    input instanceof Date
+                                ) {
+                                    return input;
+                                }
+                                if (input === null) {
+                                    return null;
+                                }
+                                context.errors.push({
+                                    instancePath: '',
+                                    schemaPath: '/type',
+                                    message: `Expected instanceof Date, ISO Date string, or null. Got ${typeof input}.`,
+                                });
+                                return undefined;
+                            },
+                            code,
+                        };
+                    }
+                    return {
+                        fn(input, context) {
+                            if (typeof input === 'string') {
+                                const decodedInput = new Date(input);
+                                if (!Number.isNaN(decodedInput.getMonth())) {
+                                    return decodedInput;
+                                }
+                            }
+                            if (typeof input === 'number') {
+                                const decodedInput = new Date(input);
+                                if (!Number.isNaN(decodedInput.getMonth())) {
+                                    return decodedInput;
+                                }
+                            }
+                            if (
+                                typeof input === 'object' &&
+                                input instanceof Date
+                            ) {
+                                return input;
+                            }
+                            context.errors.push({
+                                instancePath: '',
+                                schemaPath: '/type',
+                                message: `Expected instanceof Date or ISO Date string. Got ${typeof input}.`,
+                            });
+                            return undefined;
+                        },
+                        code,
+                    };
+                }
                 if (schema.nullable) {
                     return {
                         fn(input, context) {
@@ -534,7 +727,6 @@ export function getCompiledParser<TSchema extends ASchema<any>>(
             code,
         };
     }
-
     return { fn: new Function(input, 'context', code) as any, code };
 }
 
@@ -611,17 +803,26 @@ function bigIntDecoder(
         try {
             const val = BigInt(input);
             if (isUnsigned) {
-                if (val >= BigInt('0')) {
+                if (val >= uint64Min && val <= uint64Max) {
                     return val;
                 }
                 context.errors.push({
-                    message: 'uint64 must be greater than or equal to 0',
+                    message:
+                        'uint64 must an integer between 0 and 18,446,744,073,709,551,615',
                     schemaPath: '/type',
                     instancePath: '',
                 });
                 return undefined;
             }
-            return val;
+            if (val >= int64Min && val <= int64Max) {
+                return val;
+            }
+            context.errors.push({
+                schemaPath: '/type',
+                instancePath: '/',
+                message: `int64 must be an integer between -9,223,372,036,854,775,808 and 9,223,372,036,854,775,807`,
+            });
+            return undefined;
         } catch (err) {
             context.errors.push({
                 schemaPath: '/type',
@@ -634,23 +835,31 @@ function bigIntDecoder(
     }
     if (typeof input === 'bigint') {
         if (isUnsigned) {
-            if (input >= BigInt('0')) {
+            if (input >= uint64Min && input <= uint64Max) {
                 return input;
             }
             context.errors.push({
                 message: 'uint64 must be greater than or equal to 0',
                 schemaPath: '/type',
-                instancePath: '',
+                instancePath: '/',
             });
         }
-        return input;
+        if (input >= int64Min && input <= int64Max) {
+            return input;
+        }
+        context.errors.push({
+            schemaPath: '/type',
+            instancePath: '/',
+            message: `int64 must be an integer between -9,223,372,036,854,775,808 and 9,223,372,036,854,775,807`,
+        });
+        return undefined;
     }
     if (isNullable && input === null) {
         return null;
     }
     context.errors.push({
         schemaPath: '/type',
-        instancePath: '',
+        instancePath: '/',
         message: 'Expected BigInt or Integer string',
     });
     return undefined;
diff --git a/languages/ts/ts-schema/src/compiler/common.ts b/languages/ts/ts-schema/src/compiler/common.ts
index 654f6f43..910ee36a 100644
--- a/languages/ts/ts-schema/src/compiler/common.ts
+++ b/languages/ts/ts-schema/src/compiler/common.ts
@@ -10,4 +10,5 @@ export interface TemplateInput<TSchema extends Schema = any> {
     discriminatorValue?: string;
     subFunctions: Record<string, string>;
     finalFunctionBody?: string;
+    shouldCoerce: boolean | undefined;
 }
diff --git a/languages/ts/ts-schema/src/compiler/parse.ts b/languages/ts/ts-schema/src/compiler/parse.ts
index 763f8a71..78518c0b 100644
--- a/languages/ts/ts-schema/src/compiler/parse.ts
+++ b/languages/ts/ts-schema/src/compiler/parse.ts
@@ -35,7 +35,11 @@ import {
 } from '../lib/numberConstants';
 import { type TemplateInput } from './common';
 
-export function createParsingTemplate(input: string, schema: Schema): string {
+export function createParsingTemplate(
+    input: string,
+    schema: Schema,
+    shouldCoerce = false,
+): string {
     const fallbackTemplate = `
     function $fallback(instancePath, schemaPath, message) {
         context.errors.push({
@@ -54,6 +58,7 @@ export function createParsingTemplate(input: string, schema: Schema): string {
         instancePath: '',
         schemaPath: '',
         subFunctions,
+        shouldCoerce: shouldCoerce,
     });
     const jsonTemplate = schemaTemplate({
         val: 'json',
@@ -62,6 +67,7 @@ export function createParsingTemplate(input: string, schema: Schema): string {
         instancePath: '',
         schemaPath: '',
         subFunctions,
+        shouldCoerce: shouldCoerce,
     });
 
     if (
@@ -164,6 +170,25 @@ export function anyTemplate(input: TemplateInput<SchemaFormEmpty>): string {
 export function booleanTemplate(input: TemplateInput<SchemaFormType>): string {
     const errorMessage = 'expected boolean';
     const templateParts: string[] = [];
+    if (input.shouldCoerce) {
+        templateParts.push(`if (${input.val} === true || ${input.val} === 'true' || ${input.val} === 'TRUE' || ${input.val} === 1 || ${input.val} === '1') {
+                ${input.targetVal} = true;
+            }
+            else if (${input.val} === false || ${input.val} === 'false' || ${input.val} === 'FALSE' || ${input.val} === 0 || ${input.val} === '0') {
+                ${input.targetVal} = false;
+            }`);
+        if (input.schema.nullable) {
+            templateParts.push(`else if (${input.val} === null || ${input.val} === 'null') {
+                    ${input.targetVal} = null;
+                }`);
+        }
+        templateParts.push(
+            `else {
+                $fallback("${input.instancePath}", "${input.schemaPath}/type", "${errorMessage}");
+            }`,
+        );
+        return templateParts.join('\n');
+    }
     if (input.instancePath.length === 0) {
         templateParts.push(`if (typeof ${input.val} === 'string') {
             if (${input.val} === 'true') {
@@ -225,19 +250,21 @@ function getVarName(input: string): string {
 export function floatTemplate(input: TemplateInput<SchemaFormType>): string {
     const valName = getVarName(`${input.val}Val`);
     const templateParts: string[] = [];
-    if (input.instancePath.length === 0) {
+    if (input.instancePath.length === 0 || input.shouldCoerce) {
         templateParts.push(`if (typeof ${input.val} === 'string') {
             const ${valName} = Number(${input.val});
-            if (!Number.isNaN(parsedVal)) {
+            if (!Number.isNaN(${valName})) {
                 ${input.targetVal} = ${valName};
             }`);
         if (input.schema.nullable) {
-            templateParts.push(`if (${input.val} === 'null') {
+            templateParts.push(`else if (${input.val} === 'null') {
                 ${input.targetVal} = null;
             }`);
         }
         templateParts.push(
-            `$fallback("${input.instancePath}", "${input.schemaPath}/type", \`Could not parse number from \${${input.val}}\`);`,
+            `else { 
+                $fallback("${input.instancePath}", "${input.schemaPath}/type", \`Could not parse number from \${${input.val}}\`);
+            }`,
         );
         templateParts.push('}');
     }
@@ -248,13 +275,15 @@ export function floatTemplate(input: TemplateInput<SchemaFormType>): string {
         $fallback("${input.instancePath}", "${input.schemaPath}/type", "${errorMessage}");
     }`;
     if (input.schema.nullable) {
-        templateParts.push(`if (${input.val} === null) {
+        templateParts.push(`${input.instancePath.length === 0 || input.shouldCoerce ? 'else ' : ''}if (${input.val} === null) {
             ${input.targetVal} = null;
         } else {
             ${mainTemplate}
         }`);
     } else {
-        templateParts.push(mainTemplate);
+        templateParts.push(
+            `${input.instancePath.length === 0 || input.shouldCoerce ? 'else ' : ''}${mainTemplate}`,
+        );
     }
     return templateParts.join('\n');
 }
@@ -265,20 +294,22 @@ export function intTemplate(
     max: number,
 ) {
     const templateParts: string[] = [];
-
-    if (input.instancePath.length === 0) {
+    const shouldCoerce = input.shouldCoerce || input.instancePath.length === 0;
+    if (shouldCoerce) {
         templateParts.push(`if (typeof ${input.val} === 'string') {
             const parsedVal = Number(${input.val});
             if (Number.isInteger(parsedVal) && parsedVal >= ${min} && parsedVal <= ${max}) {
-                ${input.targetVal} = ${input.val};
+                ${input.targetVal} = parsedVal;
             }`);
         if (input.schema.nullable) {
-            templateParts.push(`if (${input.val} === 'null') {
+            templateParts.push(`else if (${input.val} === 'null') {
                 ${input.targetVal} = null;
             }`);
         }
         templateParts.push(
-            `$fallback("${input.instancePath}", "${input.schemaPath}", "Expected valid integer between ${min} and ${max});`,
+            `else {
+                $fallback("${input.instancePath}", "${input.schemaPath}", "Expected valid integer between ${min} and ${max}");
+            }`,
         );
         templateParts.push('}');
     }
@@ -287,15 +318,15 @@ export function intTemplate(
         } else {
             $fallback("${input.instancePath}", "${input.schemaPath}", "Expected valid integer between ${min} and ${max}");
         }`;
-
+    const prefix = shouldCoerce ? `else ` : '';
     if (input.schema.nullable) {
-        templateParts.push(`if (${input.val} === null) {
+        templateParts.push(`${prefix}if (${input.val} === null) {
             ${input.targetVal} = null;
         } else {
             ${mainTemplate}
         }`);
     } else {
-        templateParts.push(mainTemplate);
+        templateParts.push(prefix + mainTemplate);
     }
     return templateParts.join('\n');
 }
@@ -308,7 +339,10 @@ export function bigIntTemplate(
     templateParts.push(
         `if (typeof ${input.val} === 'string' || typeof ${input.val} === 'number') {`,
     );
-    if (input.instancePath.length === 0 && input.schema.nullable) {
+    if (
+        (input.instancePath.length === 0 || input.shouldCoerce) &&
+        input.schema.nullable
+    ) {
         templateParts.push(`if (${input.val} === 'null') {
             ${input.targetVal} = null;
         } else {`);
@@ -327,7 +361,10 @@ export function bigIntTemplate(
     templateParts.push(`} catch(err) {
         $fallback("${input.instancePath}", "${input.schemaPath}", \`Unable to parse BigInt from \${${input.val}}\`);
     }`);
-    if (input.instancePath.length === 0 && input.schema.nullable) {
+    if (
+        (input.instancePath.length === 0 || input.shouldCoerce) &&
+        input.schema.nullable
+    ) {
         templateParts.push('}');
     }
     templateParts.push('}');
@@ -357,6 +394,43 @@ export function bigIntTemplate(
 
 export function timestampTemplate(input: TemplateInput<SchemaFormType>) {
     const templateParts: string[] = [];
+    if (input.shouldCoerce) {
+        return `if (typeof ${input.val} === 'string') {
+            try {
+                const parsedVal = new Date(${input.val});
+                if (!Number.isNaN(parsedVal.getMonth())) {
+                    ${input.targetVal} = parsedVal;
+                } ${
+                    input.schema.nullable
+                        ? `else if (${input.val} === 'null') {
+                                ${input.targetVal} = null;
+                            }`
+                        : ''
+                } else {
+                    $fallback("${input.instancePath}", "${input.schemaPath}", "Unable to coerce date"); 
+                }
+            } catch (err) {
+                $fallback("${input.instancePath}", "${input.schemaPath}", "Unable to parse date"); 
+            }    
+        } else if (typeof ${input.val} === 'number') {
+            const parsedVal = new Date(${input.val});
+            if (!Number.isNaN(parsedVal.getMonth())) {
+                ${input.targetVal} = parsedVal;
+            } else {
+                $fallback("${input.instancePath}", "${input.schemaPath}", "Unable to parse date");  
+            }
+        } else if (typeof ${input.val} === 'object' && ${input.val} instanceof Date) {
+            ${input.targetVal} = ${input.val};
+        }${
+            input.schema.nullable
+                ? ` else if (${input.val} === null) {
+                    ${input.targetVal} = null;    
+                }`
+                : ''
+        } else {
+                $fallback("${input.instancePath}", "${input.schemaPath}", "Unable to parse date");  
+            }`;
+    }
     if (input.instancePath.length === 0) {
         templateParts.push(`if (typeof ${input.val} === 'string') {
             const parsedVal = new Date(${input.val});
@@ -392,7 +466,7 @@ export function timestampTemplate(input: TemplateInput<SchemaFormType>) {
     return templateParts.join('\n');
 }
 
-function enumTemplate(input: TemplateInput<SchemaFormEnum>): string {
+export function enumTemplate(input: TemplateInput<SchemaFormEnum>): string {
     const enumTemplate = input.schema.enum
         .map((val) => `${input.val} === "${val}"`)
         .join(' || ');
@@ -460,6 +534,7 @@ function objectTemplate(input: TemplateInput<SchemaFormProperties>): string {
     }
     for (const key of Object.keys(input.schema.properties)) {
         const subSchema = input.schema.properties[key];
+
         const innerTemplate = schemaTemplate({
             val: `${input.val}.${key}`,
             targetVal: `${innerTargetVal}.${key}`,
@@ -467,6 +542,7 @@ function objectTemplate(input: TemplateInput<SchemaFormProperties>): string {
             instancePath: `${input.instancePath}/${key}`,
             schemaPath: `${input.schemaPath}/properties/${key}`,
             subFunctions: input.subFunctions,
+            shouldCoerce: input.shouldCoerce,
         });
         parsingParts.push(innerTemplate);
     }
@@ -480,6 +556,7 @@ function objectTemplate(input: TemplateInput<SchemaFormProperties>): string {
                 instancePath: `${input.instancePath}/${key}`,
                 schemaPath: `${input.schemaPath}/optionalProperties/${key}`,
                 subFunctions: input.subFunctions,
+                shouldCoerce: input.shouldCoerce,
             });
             parsingParts.push(`if (typeof ${input.val}.${key} === 'undefined') {
                 // ignore undefined
@@ -541,6 +618,7 @@ export function arrayTemplate(
         schemaPath: `${input.schemaPath}/elements`,
         schema: input.schema.elements,
         subFunctions: input.subFunctions,
+        shouldCoerce: input.shouldCoerce,
     });
     const mainTemplate = `if (Array.isArray(${input.val})) {
         const ${resultVar} = [];
@@ -580,6 +658,7 @@ export function discriminatorTemplate(
             discriminatorKey: input.schema.discriminator,
             discriminatorValue: type,
             subFunctions: input.subFunctions,
+            shouldCoerce: input.shouldCoerce,
         });
         switchParts.push(`case "${type}": {
             ${template}
@@ -651,6 +730,7 @@ export function recordTemplate(input: TemplateInput<SchemaFormValues>): string {
         schemaPath: `${input.schemaPath}/values`,
         schema: input.schema.values,
         subFunctions: input.subFunctions,
+        shouldCoerce: input.shouldCoerce,
     });
     const mainTemplate = `if (typeof ${input.val} === 'object' && ${input.val} !== null) {
         const ${resultVal} = {};
diff --git a/languages/ts/ts-schema/src/compiler/serialize.ts b/languages/ts/ts-schema/src/compiler/serialize.ts
index ea7163ba..c7c28ef6 100644
--- a/languages/ts/ts-schema/src/compiler/serialize.ts
+++ b/languages/ts/ts-schema/src/compiler/serialize.ts
@@ -40,6 +40,7 @@ export function createSerializationV2Template(
         outputPrefix: '',
         needsSanitization: [],
         subFunctions,
+        shouldCoerce: undefined,
     };
     const result = template(context);
     if (isSchemaFormType(schema) || isSchemaFormEnum(schema)) {
@@ -313,6 +314,7 @@ export function objectTemplate(
             outputPrefix: includeComma ? `,"${key}":` : `"${key}":`,
             needsSanitization: input.needsSanitization,
             subFunctions: input.subFunctions,
+            shouldCoerce: undefined,
         });
         templateParts.push(innerTemplate);
     }
@@ -345,6 +347,7 @@ export function objectTemplate(
                 outputPrefix: `"${key}":`,
                 needsSanitization: input.needsSanitization,
                 subFunctions: input.subFunctions,
+                shouldCoerce: undefined,
             });
             const innerTemplateWithComma = template({
                 schema: optionalPropSchema,
@@ -355,6 +358,7 @@ export function objectTemplate(
                 outputPrefix: `,"${key}":`,
                 needsSanitization: input.needsSanitization,
                 subFunctions: input.subFunctions,
+                shouldCoerce: undefined,
             });
             templateParts.push(`if (typeof ${innerVal} !== 'undefined') {
                 if (${hasFieldsVar}) {
@@ -382,6 +386,7 @@ export function objectTemplate(
                 outputPrefix: `,"${key}":`,
                 needsSanitization: input.needsSanitization,
                 subFunctions: input.subFunctions,
+                shouldCoerce: undefined,
             });
             const completeInnerTemplate = `if (typeof ${innerVal} !== 'undefined') {
                 ${innerTemplate}
@@ -437,6 +442,7 @@ export function arrayTemplate(
         outputPrefix: '',
         needsSanitization: input.needsSanitization,
         subFunctions: input.subFunctions,
+        shouldCoerce: undefined,
     });
     templateParts.push(`for (let i = 0; i < ${input.val}.length; i++) {
         const ${itemVarName} = ${input.val}[i];
@@ -476,6 +482,7 @@ export function recordTemplate(
         outputPrefix: '',
         needsSanitization: input.needsSanitization,
         subFunctions: input.subFunctions,
+        shouldCoerce: undefined,
     });
     templateParts.push(`for (let i = 0; i < ${keysVarName}.length; i++) {
         const key = ${keysVarName}[i];
@@ -548,6 +555,7 @@ function discriminatorTemplate(
             outputPrefix: input.outputPrefix,
             needsSanitization: input.needsSanitization,
             subFunctions: input.subFunctions,
+            shouldCoerce: undefined,
         });
         templateParts.push(`case '${val}': {
             ${innerTemplate}
diff --git a/languages/ts/ts-schema/src/compiler/validate.ts b/languages/ts/ts-schema/src/compiler/validate.ts
index b37342b5..a14b88aa 100644
--- a/languages/ts/ts-schema/src/compiler/validate.ts
+++ b/languages/ts/ts-schema/src/compiler/validate.ts
@@ -45,6 +45,7 @@ export function createValidationTemplate(
         schemaPath: ``,
         instancePath: '',
         subFunctions,
+        shouldCoerce: undefined,
     });
 
     const subFunctionBodies = Object.keys(subFunctions).map(
@@ -196,6 +197,7 @@ function objectTemplate(input: TemplateInput<AObjectSchema<any>>): string {
                 val: `${input.val}.${key}`,
                 targetVal: '',
                 subFunctions: input.subFunctions,
+                shouldCoerce: undefined,
             }),
         );
     }
@@ -209,6 +211,7 @@ function objectTemplate(input: TemplateInput<AObjectSchema<any>>): string {
                 val: `${input.val}.${key}`,
                 targetVal: '',
                 subFunctions: input.subFunctions,
+                shouldCoerce: undefined,
             });
             parts.push(
                 `(typeof ${input.val}.${key} === 'undefined' || (${template}))`,
@@ -253,6 +256,7 @@ function arrayTemplate(input: TemplateInput<AArraySchema<any>>) {
         schema: input.schema.elements,
         targetVal: '',
         subFunctions: input.subFunctions,
+        shouldCoerce: undefined,
     });
 
     if (input.schema.nullable) {
@@ -269,6 +273,7 @@ function recordTemplate(input: TemplateInput<ARecordSchema<any>>): string {
         val: `${input.val}[key]`,
         targetVal: '',
         subFunctions: input.subFunctions,
+        shouldCoerce: undefined,
     });
     const mainTemplate = `typeof ${input.val} === 'object' && ${input.val} !== null && Object.keys(${input.val}).every((key) => ${subTemplate})`;
     if (input.schema.nullable) {
@@ -296,6 +301,7 @@ function discriminatorTemplate(
                 discriminatorKey: input.schema.discriminator,
                 discriminatorValue: discriminatorVal,
                 subFunctions: input.subFunctions,
+                shouldCoerce: undefined,
             }),
         );
     }
diff --git a/languages/ts/ts-server/src/rpc.ts b/languages/ts/ts-server/src/rpc.ts
index b98e1c29..1ebf6471 100644
--- a/languages/ts/ts-server/src/rpc.ts
+++ b/languages/ts/ts-server/src/rpc.ts
@@ -390,7 +390,7 @@ export function getSchemaValidator<T extends Record<string, any> = any>(
                 validate: validator.validate,
                 serialize: validator.serialize,
                 parse: validator.parse,
-                coerce: (input) => a.coerce(schema, input),
+                coerce: validator.coerce,
                 errors: (input) => a.errors(schema, input),
             };
         } catch (err) {


Test patch:
diff --git a/languages/ts/ts-schema/src/compiler/parse.test.ts b/languages/ts/ts-schema/src/compiler/parse.test.ts
index 59093c42..4ff7ba06 100644
--- a/languages/ts/ts-schema/src/compiler/parse.test.ts
+++ b/languages/ts/ts-schema/src/compiler/parse.test.ts
@@ -2,7 +2,11 @@ import { isEqual } from 'lodash';
 
 import { a } from '../_index';
 import { compile } from '../compile';
-import { parsingTestSuites, validationTestSuites } from '../testSuites';
+import {
+    coercionTestSuites,
+    parsingTestSuites,
+    validationTestSuites,
+} from '../testSuites';
 
 for (const key of Object.keys(validationTestSuites)) {
     const suite = validationTestSuites[key]!;
@@ -77,6 +81,61 @@ describe('parsing test suites', () => {
     }
 });
 
+describe('coercion test suites', () => {
+    for (const key of Object.keys(coercionTestSuites)) {
+        const suite = coercionTestSuites[key]!;
+        describe(key, () => {
+            const Compiled = compile(suite.schema);
+            for (let i = 0; i < suite.goodInputs.length; i++) {
+                test(`${key} - Good Input ${i}`, () => {
+                    const input = suite.goodInputs[i];
+                    const expectedResult = suite.expectedResults[i];
+                    const actualResult = Compiled.coerce(input);
+                    if (!actualResult.success) {
+                        console.log({
+                            schema: suite.schema,
+                            input: input,
+                            inputIndex: i,
+                            errors: actualResult.errors,
+                        });
+                        console.log(input, 'Should coerce');
+                    }
+                    expect(actualResult.success).toBe(true);
+                    if (!actualResult.success) return;
+                    const serializedResult = Compiled.serialize(
+                        actualResult.value,
+                    );
+                    expect(serializedResult.success).toBe(true);
+                    if (!serializedResult.success) return;
+                    expect(actualResult.value).toStrictEqual(expectedResult);
+                    expect(
+                        Compiled.coerceUnsafe(serializedResult.value),
+                    ).toStrictEqual(expectedResult);
+                    expect(actualResult.value).toStrictEqual(
+                        a.coerceUnsafe(suite.schema, input),
+                    );
+                });
+            }
+            for (let i = 0; i < suite.badInputs.length; i++) {
+                test(`${key} - Bad Input ${i}`, () => {
+                    const input = suite.badInputs[i];
+                    const result = Compiled.coerce(input);
+                    if (result.success) {
+                        console.log({
+                            schema: suite.schema,
+                            input: input,
+                            inputIndex: i,
+                            result: result.value,
+                        });
+                        console.log(input, 'Should NOT coerce');
+                    }
+                    expect(result.success).toBe(false);
+                });
+            }
+        });
+    }
+});
+
 it('parses floats', () => {
     const Compiled = a.compile(a.number());
     expect(Compiled.parseUnsafe('1')).toBe(1);
diff --git a/languages/ts/ts-schema/src/lib/validation.test.ts b/languages/ts/ts-schema/src/lib/validation.test.ts
index ada2853e..e8959144 100644
--- a/languages/ts/ts-schema/src/lib/validation.test.ts
+++ b/languages/ts/ts-schema/src/lib/validation.test.ts
@@ -2,6 +2,7 @@ import { isEqual } from 'lodash';
 
 import { a } from '../_index';
 import {
+    coercionTestSuites,
     parsingTestSuites,
     serializationTestSuites,
     validationTestSuites,
@@ -40,6 +41,26 @@ describe('parsing test suites', () => {
     }
 });
 
+describe('coercion test suites', () => {
+    for (const key of Object.keys(coercionTestSuites)) {
+        const suite = coercionTestSuites[key]!;
+        test(key, () => {
+            for (let i = 0; i < suite.goodInputs.length; i++) {
+                const input = suite.goodInputs[i];
+                const result = a.coerce(suite.schema, input);
+                if (!result.success)
+                    console.error('KEY', key, 'INDEX', i, result.errors);
+                expect(result.success).toBe(true);
+                if (result.success) {
+                    expect(result.value).toStrictEqual(
+                        suite.expectedResults[i],
+                    );
+                }
+            }
+        });
+    }
+});
+
 describe('serialization test suites', () => {
     for (const key of Object.keys(serializationTestSuites)) {
         const suite = serializationTestSuites[key]!;
diff --git a/languages/ts/ts-schema/src/testSuites.ts b/languages/ts/ts-schema/src/testSuites.ts
index d42d738a..cfd878c1 100644
--- a/languages/ts/ts-schema/src/testSuites.ts
+++ b/languages/ts/ts-schema/src/testSuites.ts
@@ -1246,6 +1246,463 @@ export const parsingTestSuites: Record<
     },
 };
 
+export const coercionTestSuites: Record<
+    string,
+    {
+        schema: ASchema;
+        goodInputs: any[];
+        expectedResults: any[];
+        badInputs: any[];
+    }
+> = {
+    boolean: {
+        schema: a.boolean(),
+        goodInputs: [
+            true,
+            false,
+            'true',
+            'TRUE',
+            'false',
+            'FALSE',
+            1,
+            0,
+            '1',
+            '0',
+        ],
+        expectedResults: [
+            true,
+            false,
+            true,
+            true,
+            false,
+            false,
+            true,
+            false,
+            true,
+            false,
+        ],
+        badInputs: ['hello world', 100, -100, {}],
+    },
+    timestamp: {
+        schema: a.timestamp(),
+        goodInputs: ['2001-01-01T00:00:00.000Z', 1000],
+        expectedResults: [new Date('2001-01-01T00:00:00.000Z'), new Date(1000)],
+        badInputs: ['hello world', true, false, [], {}],
+    },
+    int8: {
+        schema: a.int8(),
+        goodInputs: [1, '1', '15', 15, '-15', -15],
+        expectedResults: [1, 1, 15, 15, -15, -15],
+        badInputs: ['hello world', 1.1, '1.1', 1000, '1000'],
+    },
+    uint8: {
+        schema: a.uint8(),
+        goodInputs: [1, '1', '15', 15],
+        expectedResults: [1, 1, 15, 15],
+        badInputs: [
+            'hello world',
+            1.1,
+            '1.1',
+            -1,
+            '-1',
+            -15,
+            '-15',
+            [],
+            {},
+            true,
+        ],
+    },
+    int16: {
+        schema: a.int16(),
+        goodInputs: [1, '1', '15', 15, '-15', -15],
+        expectedResults: [1, 1, 15, 15, -15, -15],
+        badInputs: ['hello world', 1.1, '1.1', 40000, '-40000'],
+    },
+    uint16: {
+        schema: a.uint16(),
+        goodInputs: [1, '1', '15', 15],
+        expectedResults: [1, 1, 15, 15],
+        badInputs: [
+            'hello world',
+            1.1,
+            '1.1',
+            -1,
+            '-1',
+            -15,
+            '-15',
+            [],
+            {},
+            true,
+            100000,
+            '1000000',
+        ],
+    },
+    int32: {
+        schema: a.int32(),
+        goodInputs: [1, '1', '15', 15, '-15', -15],
+        expectedResults: [1, 1, 15, 15, -15, -15],
+        badInputs: ['hello world', 3147483647, '3147483647'],
+    },
+    uint32: {
+        schema: a.uint32(),
+        goodInputs: [1, '1', '15', 15],
+        expectedResults: [1, 1, 15, 15],
+        badInputs: [
+            'hello world',
+            -1,
+            '-1',
+            -15,
+            '-15',
+            [],
+            {},
+            true,
+            5294967295,
+            '5294967295',
+        ],
+    },
+    int64: {
+        schema: a.int64(),
+        goodInputs: ['1', 1, '-1', -1],
+        expectedResults: [BigInt(1), BigInt(1), BigInt(-1), BigInt(-1)],
+        badInputs: [
+            'hello world',
+            BigInt('10000000000000000000'),
+            '10000000000000000000',
+            true,
+            [],
+            {},
+        ],
+    },
+    uint64: {
+        schema: a.uint64(),
+        goodInputs: ['1', 1],
+        expectedResults: [BigInt(1), BigInt(1)],
+        badInputs: [
+            'hello world',
+            BigInt('20000000000000000000'),
+            '20000000000000000000',
+            '-1',
+            BigInt(-1),
+            true,
+            [],
+            {},
+        ],
+    },
+    object: {
+        schema: a.object({
+            boolean: a.boolean(),
+            timestamp: a.timestamp(),
+            float32: a.float32(),
+            float64: a.float64(),
+            int8: a.int8(),
+            uint8: a.uint8(),
+            int16: a.int16(),
+            uint16: a.uint16(),
+            int32: a.int32(),
+            uint32: a.uint32(),
+            int64: a.int64(),
+            uint64: a.uint64(),
+            array: a.array(a.boolean()),
+        }),
+        goodInputs: [
+            {
+                boolean: 'true',
+                timestamp: 1000,
+                float32: '1.5',
+                float64: '1.5',
+                int8: '1',
+                uint8: '1',
+                int16: '10',
+                uint16: '10',
+                int32: '100',
+                uint32: '100',
+                int64: '1000',
+                uint64: '1000',
+                array: ['true', 'TRUE', 'false', 'FALSE', 1, 0, '1', '0'],
+            },
+        ],
+        expectedResults: [
+            {
+                boolean: true,
+                timestamp: new Date(1000),
+                float32: 1.5,
+                float64: 1.5,
+                int8: 1,
+                uint8: 1,
+                int16: 10,
+                uint16: 10,
+                int32: 100,
+                uint32: 100,
+                int64: 1000n,
+                uint64: 1000n,
+                array: [true, true, false, false, true, false, true, false],
+            },
+        ],
+        badInputs: [
+            {
+                boolean: 'true',
+                timestamp: 1000,
+                float32: '1.5',
+                float64: '1.5',
+                int8: '1',
+                uint8: '1',
+                int16: '10',
+                uint16: '10',
+                int32: '100',
+                uint32: '100',
+                int64: '1000',
+                uint64: '1000',
+                array: [
+                    'true',
+                    'TRUE',
+                    'false',
+                    'FALSE',
+                    1,
+                    0,
+                    '1',
+                    '0',
+                    'hello world',
+                ],
+            },
+            {
+                boolean: 'true',
+                timestamp: 1000,
+                float32: '1.5',
+                float64: '1.5',
+                int8: '1',
+                uint8: '1',
+                int16: '10',
+                uint16: '10',
+                int32: '100',
+                uint32: '100',
+                int64: '1000',
+                uint64: '1000',
+                array: ['null', null, 'false', 'FALSE', 1, 0, '1', '0'],
+            },
+        ],
+    },
+    'object with optional fields': {
+        schema: a.partial(
+            a.object({
+                boolean: a.boolean(),
+                timestamp: a.timestamp(),
+                float32: a.float32(),
+                float64: a.float64(),
+                int8: a.int8(),
+                uint8: a.uint8(),
+                int16: a.int16(),
+                uint16: a.uint16(),
+                int32: a.int32(),
+                uint32: a.uint32(),
+                int64: a.int64(),
+                uint64: a.uint64(),
+                array: a.array(a.boolean()),
+            }),
+        ),
+        goodInputs: [
+            {
+                boolean: 'true',
+                timestamp: 1000,
+                float32: '1.5',
+                float64: '1.5',
+                int8: '1',
+                uint8: '1',
+                int16: '10',
+                uint16: '10',
+                int32: '100',
+                uint32: '100',
+                int64: '1000',
+                uint64: '1000',
+                array: ['true', 'TRUE', 'false', 'FALSE', 1, 0, '1', '0'],
+            },
+            {},
+        ],
+        expectedResults: [
+            {
+                boolean: true,
+                timestamp: new Date(1000),
+                float32: 1.5,
+                float64: 1.5,
+                int8: 1,
+                uint8: 1,
+                int16: 10,
+                uint16: 10,
+                int32: 100,
+                uint32: 100,
+                int64: 1000n,
+                uint64: 1000n,
+                array: [true, true, false, false, true, false, true, false],
+            },
+            {},
+        ],
+        badInputs: [
+            {
+                boolean: 'true',
+                timestamp: 1000,
+                float32: '1.5',
+                float64: '1.5',
+                int8: '1',
+                uint8: '1',
+                int16: '10',
+                uint16: '10',
+                int32: '100',
+                uint32: '100',
+                int64: '1000',
+                uint64: '1000',
+                array: [
+                    'true',
+                    'TRUE',
+                    'false',
+                    'FALSE',
+                    1,
+                    0,
+                    '1',
+                    '0',
+                    'hello world',
+                ],
+            },
+        ],
+    },
+    'object with nullable fields': {
+        schema: a.object({
+            boolean: a.nullable(a.boolean()),
+            timestamp: a.nullable(a.timestamp()),
+            float32: a.nullable(a.float32()),
+            float64: a.nullable(a.float64()),
+            int8: a.nullable(a.int8()),
+            uint8: a.nullable(a.uint8()),
+            int16: a.nullable(a.int16()),
+            uint16: a.nullable(a.uint16()),
+            int32: a.nullable(a.int32()),
+            uint32: a.nullable(a.uint32()),
+            int64: a.nullable(a.int64()),
+            uint64: a.nullable(a.uint64()),
+            array: a.nullable(a.array(a.nullable(a.boolean()))),
+        }),
+        goodInputs: [
+            {
+                boolean: 'true',
+                timestamp: 1000,
+                float32: '1.5',
+                float64: '1.5',
+                int8: '1',
+                uint8: '1',
+                int16: '10',
+                uint16: '10',
+                int32: '100',
+                uint32: '100',
+                int64: '1000',
+                uint64: '1000',
+                array: ['true', 'TRUE', 'false', 'FALSE', 1, 0, '1', '0'],
+            },
+            {
+                boolean: 'null',
+                timestamp: 'null',
+                float32: 'null',
+                float64: 'null',
+                int8: 'null',
+                uint8: 'null',
+                int16: 'null',
+                uint16: 'null',
+                int32: 'null',
+                uint32: 'null',
+                int64: 'null',
+                uint64: 'null',
+                array: ['null', null],
+            },
+            {
+                boolean: null,
+                timestamp: null,
+                float32: null,
+                float64: null,
+                int8: null,
+                uint8: null,
+                int16: null,
+                uint16: null,
+                int32: null,
+                uint32: null,
+                int64: null,
+                uint64: null,
+                array: ['null', null, true, false, 'TRUE', 'FALSE'],
+            },
+        ],
+        expectedResults: [
+            {
+                boolean: true,
+                timestamp: new Date(1000),
+                float32: 1.5,
+                float64: 1.5,
+                int8: 1,
+                uint8: 1,
+                int16: 10,
+                uint16: 10,
+                int32: 100,
+                uint32: 100,
+                int64: 1000n,
+                uint64: 1000n,
+                array: [true, true, false, false, true, false, true, false],
+            },
+            {
+                boolean: null,
+                timestamp: null,
+                float32: null,
+                float64: null,
+                int8: null,
+                uint8: null,
+                int16: null,
+                uint16: null,
+                int32: null,
+                uint32: null,
+                int64: null,
+                uint64: null,
+                array: [null, null],
+            },
+            {
+                boolean: null,
+                timestamp: null,
+                float32: null,
+                float64: null,
+                int8: null,
+                uint8: null,
+                int16: null,
+                uint16: null,
+                int32: null,
+                uint32: null,
+                int64: null,
+                uint64: null,
+                array: [null, null, true, false, true, false],
+            },
+        ],
+        badInputs: [
+            {
+                boolean: 'true',
+                timestamp: 1000,
+                float32: '1.5',
+                float64: '1.5',
+                int8: '1',
+                uint8: '1',
+                int16: '10',
+                uint16: '10',
+                int32: '100',
+                uint32: '100',
+                int64: '1000',
+                uint64: '1000',
+                array: [
+                    'true',
+                    'TRUE',
+                    'false',
+                    'FALSE',
+                    1,
+                    0,
+                    '1',
+                    '0',
+                    'hello world',
+                ],
+            },
+        ],
+    },
+};
+
 export const serializationTestSuites: Record<
     string,
     { schema: ASchema; inputs: any[] }