Repository: elysiajs/elysia (TypeScript)

Issue:
Default validation values doesn't work with `aot: false`
### What version of Elysia is running?

1.1.26

### What platform is your computer?

WSL Ubuntu

### What steps can reproduce the bug?

```ts
// test.test.ts
import { test, expect } from "bun:test";
import { Elysia, t } from "elysia";

test("Default values with aot: false", async () => {
  const app = new Elysia({ aot: false }).get(
    "/",
    ({ query: { limit } }) => limit,
    {
      headers: t.Object({
        yay: t.String({
          default: "yay",
        }),
      }),
      query: t.Object({
        limit: t.String({
          default: "yay",
        }),
      }),
    }
  );

  await app.modules;

  const response = await app.handle(new Request(`http://localhost`));

  expect(response.status).toBe(200);
});
```

### What is the expected behavior?

test pass

### What do you see instead?

test fail

```
Expected: 200
Received: 422
```

### Additional information

It doesn't work for both `headers` and `query`, maybe others too. You can just comment out parts of the above code sample.

---

reported on [discord](https://discord.com/channels/1044804142461362206/1044804143455420539/1319352533403177011)

### Have you try removing the `node_modules` and `bun.lockb` and try again yet?

yes


Discussion:
aot being off causes weird and unexpectable behaviors in many places that shouldn't be affected by it #946 #948, the implementation needs reconsideration 



Patch:
diff --git a/src/dynamic-handle.ts b/src/dynamic-handle.ts
index a7d96d17..98c5ecc8 100644
--- a/src/dynamic-handle.ts
+++ b/src/dynamic-handle.ts
@@ -16,6 +16,7 @@ import { parseCookie } from './cookies'
 
 import type { Handler, LifeCycleStore, SchemaValidator } from './types'
 import { TransformDecodeError } from '@sinclair/typebox/value'
+import { TypeCheck } from './type-system'
 
 // JIT Handler
 export type DynamicHandler = {
@@ -25,6 +26,14 @@ export type DynamicHandler = {
 	validator?: SchemaValidator
 }
 
+const injectDefaultValues = (typeChecker: TypeCheck<any>, obj: Record<string, any>) => {
+	// @ts-expect-error private
+	for (const [key, keySchema] of Object.entries(typeChecker.schema.properties)) {
+		// @ts-expect-error private
+		obj[key] ??= keySchema.default
+	}
+}
+
 export const createDynamicHandler = (app: AnyElysia) => {
 	const { mapResponse, mapEarlyResponse } = app['~adapter'].handler
 
@@ -220,6 +229,18 @@ export const createDynamicHandler = (app: AnyElysia) => {
 					: undefined
 			)) as any
 
+			const headerValidator = validator?.createHeaders?.()
+			if (headerValidator)
+				injectDefaultValues(headerValidator, context.headers)
+
+			const paramsValidator = validator?.createParams?.()
+			if (paramsValidator)
+				injectDefaultValues(paramsValidator, context.params)
+
+			const queryValidator = validator?.createQuery?.()
+			if (queryValidator)
+				injectDefaultValues(queryValidator, context.query)
+
 			for (let i = 0; i < hooks.transform.length; i++) {
 				const hook = hooks.transform[i]
 				const operation = hook.fn(context)
@@ -232,10 +253,10 @@ export const createDynamicHandler = (app: AnyElysia) => {
 			}
 
 			if (validator) {
-				if (validator.createHeaders?.()) {
-					const _header: Record<string, string> = {}
-					for (const key in request.headers)
-						_header[key] = request.headers.get(key)!
+				if (headerValidator) {
+					const _header = structuredClone(context.headers)
+					for (const [key, value] of request.headers)
+						_header[key] = value
 
 					if (validator.headers!.Check(_header) === false)
 						throw new ValidationError(
@@ -247,9 +268,7 @@ export const createDynamicHandler = (app: AnyElysia) => {
 					// @ts-ignore
 					context.headers = validator.headers.Decode(context.headers)
 
-				if (
-					validator.createParams?.()?.Check(context.params) === false
-				) {
+				if (paramsValidator?.Check(context.params) === false) {
 					throw new ValidationError(
 						'params',
 						validator.params!,
@@ -259,7 +278,7 @@ export const createDynamicHandler = (app: AnyElysia) => {
 					// @ts-ignore
 					context.params = validator.params.Decode(context.params)
 
-				if (validator.createQuery?.()?.Check(context.query) === false)
+				if (queryValidator?.Check(context.query) === false)
 					throw new ValidationError(
 						'query',
 						validator.query!,


Test patch:
diff --git a/test/core/dynamic.test.ts b/test/core/dynamic.test.ts
index b5f37cce..66092286 100644
--- a/test/core/dynamic.test.ts
+++ b/test/core/dynamic.test.ts
@@ -178,6 +178,34 @@ describe('Dynamic Mode', () => {
 		expect(res).toBe('me - v1,v2')
 	})
 
+	it('default value', async () => {
+		const app = new Elysia({ aot: false }).get(
+			"/:propParams?",
+			({ params: { propParams }, headers: { propHeader }, query: { propQuery } }) => `${propParams} ${propHeader} ${propQuery}`,
+			{
+				params: t.Object({
+					propParams: t.String({
+						default: "params-default",
+					}),
+				}),
+				headers: t.Object({
+					propHeader: t.String({
+						default: "header-default",
+					}),
+				}),
+				query: t.Object({
+					propQuery: t.String({
+						default: "query-default",
+					}),
+				}),
+			}
+		);
+
+		const response = await app.handle(new Request('http://localhost')).then((x) => x.text());
+
+		expect(response).toBe('params-default header-default query-default');
+	});
+
 	it('handle non query fallback', async () => {
 		const app = new Elysia({ aot: false }).get('/', () => 'hi', {
 			query: t.Object({
diff --git a/test/core/elysia.test.ts b/test/core/elysia.test.ts
index ea13d0fc..2a3e9ab6 100644
--- a/test/core/elysia.test.ts
+++ b/test/core/elysia.test.ts
@@ -1,4 +1,4 @@
-import { Elysia } from '../../src'
+import { Elysia, t } from '../../src'
 
 import { describe, expect, it } from 'bun:test'
 import { req } from '../utils'
@@ -173,4 +173,37 @@ describe('Edge Case', () => {
 		// @ts-expect-error private property
 		expect(main.getGlobalRoutes().length).toBe(2)
 	})
+
+	describe('value returned from transform has priority over the default value from schema', () => {
+		const route = new Elysia().get(
+			"/:propParams?",
+			({ params: { propParams } }) => propParams,
+			{
+				params: t.Object({
+					propParams: t.String({
+						default: "params-default",
+					}),
+				}),
+				transform({ params }) {
+					params.propParams = "params-transform"
+				}
+			}
+		)
+
+		it('aot is on', async () => {
+			const app = new Elysia().use(route);
+
+			const response = await app.handle(new Request('http://localhost')).then((x) => x.text());
+
+			expect(response).toBe('params-transform');
+		})
+
+		it('aot is off', async () => {
+			const app = new Elysia({ aot: false }).use(route);
+
+			const response = await app.handle(new Request('http://localhost')).then((x) => x.text());
+
+			expect(response).toBe('params-transform');
+		})
+	})
 })