Skip to content

Commit

Permalink
feat: add support for zod effectors and defaults (#8)
Browse files Browse the repository at this point in the history
Current source is lacking support for effectors and zod defaults
support, causing it to default to a base64 representation of the data
which is not ideal as something like:

```ts
const schema = z.object({
    a: z.string().default("hi"),
    b: z.number().default(1),
});
const data = {a: "test", b: 10};
const outputParams = serialize({schema, data});
```

produces the output `a=InRlc3Qi&b=MTA`
instead of `a=test&b=10`

## Expected outputs

```ts
const schema = z.object({
    a: z.string().transform(Number),
});
const input = new URLSearchParams("a=1");

const parsed = parse({schema, input})
// {a: 1}
const serailized = serialize({schema, data: parsed})
// new URLSearchParams("a=1")
```

or with defaults
```ts
const schema = z.object({
    a: z.string().default("hi"),
    b: z.number().default(1),
});
const input = new URLSearchParams("b=10");

const parsed = parse({schema, input})
// {a: "hi", b: 10}
const serialized = serialize({schema, data: parsed})
// new URLSearchParams("b=10")
```

Note: both are also supported in serialize too, defaults will be omitted
if they match (to reduce the url length, following the same existing
behaviour in the optional `defaults` argument)
  • Loading branch information
hollandjake authored Nov 27, 2024
1 parent fe96695 commit 9886757
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 5 deletions.
26 changes: 23 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ function parseValue(value: string, schemaType: z.ZodTypeAny): unknown {
if (schemaType instanceof z.ZodBigInt) {
return stringToBigInt.parse(value)
}
if (schemaType instanceof z.ZodEffects) {
return parseValue(value, schemaType._def.schema)
}
if (schemaType instanceof z.ZodDefault) {
return parseValue(value, schemaType._def.innerType)
}
if (
schemaType instanceof z.ZodEnum ||
schemaType instanceof z.ZodNativeEnum ||
Expand All @@ -92,7 +98,7 @@ function parseValue(value: string, schemaType: z.ZodTypeAny): unknown {
return stringToOther.parse(value)
}

function serializeValue(value: unknown, schemaType: z.ZodTypeAny): string {
function serializeValue(value: unknown, schemaType: z.ZodTypeAny): string | undefined {
if (
schemaType instanceof z.ZodString ||
schemaType instanceof z.ZodEnum ||
Expand Down Expand Up @@ -120,6 +126,18 @@ function serializeValue(value: unknown, schemaType: z.ZodTypeAny): string {
if (schemaType instanceof z.ZodBigInt) {
return bigIntToString.parse(value)
}
if (schemaType instanceof z.ZodEffects) {
// For effectors, we use the output type, effectively ignoring the effector logic
return serializeValue(value, schemaType._def.schema)
}
if (schemaType instanceof z.ZodDefault) {
// Serialize the value according to the defaults value
// If the serialized default is the same then we skip it
let serialized = serializeValue(value, schemaType._def.innerType);
let defaultValue = serializeValue(schemaType._def.defaultValue(), schemaType._def.innerType);
if (serialized === defaultValue) return undefined;
return serialized;
}
return otherToString.parse(value)
}

Expand Down Expand Up @@ -226,10 +244,12 @@ function serialize<T extends Schema>({
if (defaultData == null || !isEqual(value, defaultData[key])) {
if (schemaType instanceof z.ZodArray) {
for (let item of value as unknown[]) {
params.append(key, serializeValue(item, schemaType.element))
let serialized = serializeValue(item, schemaType.element);
if (serialized !== undefined) params.append(key, serialized)
}
} else {
params.append(key, serializeValue(value, schemaType))
let serialized = serializeValue(value, schemaType);
if (serialized !== undefined) params.append(key, serialized)
}
}
}
Expand Down
78 changes: 76 additions & 2 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assert, expect, test } from "vitest"
import { z } from "zod"
import { assert, expect, test, describe } from "vitest"
import {z, ZodError} from "zod"
import { ZodURLSearchParamSerializer, lenientParse, parse, safeParse, serialize } from "../src"

test("serialize basic object", () => {
Expand Down Expand Up @@ -498,3 +498,77 @@ test("parse URLSearchParams with array of strings and array of numbers", () => {

assert.deepEqual(result, expected)
})

describe("with effects", () => {
test.for([
{
name: 'default',
schema: z.object({
a: z.string().default("hi"),
b: z.number().default(1),
}),
input: new URLSearchParams(),
expected: {a: "hi", b: 1},
},
{
name: 'optional',
schema: z.object({
a: z.string().optional(),
b: z.number().optional(),
}),
input: new URLSearchParams(),
expected: {},
},
{
name: 'min',
schema: z.object({
a: z.string().min(1),
b: z.number().min(1),
}),
input: new URLSearchParams("a=hi&b=1"),
expected: {a: "hi", b: 1},
},
{
name: 'min condition not met',
schema: z.object({
a: z.string().min(1),
b: z.number().min(1),
}),
input: new URLSearchParams("a=&b=0"),
expected: ZodError,
},
{
name: 'preprocessor',
schema: z.object({
a: z.preprocess(Number, z.number()),
}),
input: new URLSearchParams("a=1"),
expected: {a: 1},
},
{
name: 'post processor - transform number->number',
schema: z.object({
a: z.number().transform(Number),
}),
input: new URLSearchParams("a=1"),
expected: {a: 1},
},
{
name: 'post processor - transform string->number',
schema: z.object({
a: z.string().transform(Number),
}),
input: new URLSearchParams("a=1"),
expected: {a: 1},
},
])(`$name`, ({schema, input, expected}, {expect}) => {
if (expected instanceof Error || Error.isPrototypeOf(expected)) {
expect(() => parse({schema, input})).toThrow(expected as never)
} else {
let parsed = parse({schema, input: new URLSearchParams(input)});
expect(parsed).toEqual(expected)
let urlSearchParams = serialize({schema, data: parsed});
expect(urlSearchParams).toEqual(input)
}
})
})

0 comments on commit 9886757

Please sign in to comment.