Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__snapshots__
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,10 @@
"prepublishOnly": "run-s -l test build"
},
"peerDependencies": {
"zod": "^3.24.2"
"zod": "^4.0.0"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.17.4",
"@arethetypeswrong/cli": "^0.18.2",
"@types/jest": "^29.5.14",
"@types/node": "^22.13.10",
"eslint": "^9.22.0",
Expand All @@ -79,7 +79,7 @@
"ts-jest": "^29.2.6",
"ts-node": "^10.9.2",
"typescript": "^5.8.2",
"zod": "~3.24.2"
"zod": "^4"
},
"jest": {
"preset": "ts-jest/presets/default-esm",
Expand All @@ -100,5 +100,5 @@
]
}
},
"packageManager": "pnpm@10.6.5+sha512.cdf928fca20832cd59ec53826492b7dc25dc524d4370b6b4adbf65803d32efaa6c1c88147c0ae4e8d579a6c9eec715757b50d4fa35eea179d868eada4ed043af"
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
}
41 changes: 21 additions & 20 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions src/__snapshots__/reporter.test.ts.snap

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I adjusted the formatter a bit to closer match what zod's prettifyError outputs. We did not had any test before how the output looks like, so the test covers that.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`reporter should test the error output 1`] = `
"Errors found while parsing environment:
[CONFIG_OBJECT]: This is a very special object that holds configuration.
✖ Invalid input: expected string, received number
→ at a
✖ Invalid input: expected number, received undefined
Comment thread
BowlingX marked this conversation as resolved.
→ at b
✖ Invalid input: expected string, received number
→ at c.d
(received "{\\"a\\":123,\\"c\\":{\\"d\\":21}}")

[FOO]:
✖ Invalid input: expected number, received string
(received "bar")

[SOME_WITH_DEPRECATED_DESCRIPTION]: Some description
✖ Invalid input: expected string, received undefined
(received undefined)

[SOME_WITH_DEPRECATED_DESCRIPTION_AND_NEW_DESCRIPTION]: New description
✖ Invalid input: expected string, received undefined
(received undefined)

[PORT]:
✖ Too big: expected number to be <=65535
(received undefined)
(used default of 70000)
"
`;
4 changes: 2 additions & 2 deletions src/extra-schemas.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as z from "zod";
import * as z from "zod/v4";

export const port = () => z.number().int().nonnegative().lte(65535);

export const deprecate = () =>
z
.undefined({ invalid_type_error: "This var is deprecated." })
.undefined({ error: "This var is deprecated." })
.transform(() => undefined as never);
2 changes: 1 addition & 1 deletion src/parse-env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ describe("parseCore", () => {
).toThrow();
});

const schemasWithDefaults: [z.ZodTypeAny, any][] = [
const schemasWithDefaults: [z.ZodType, any][] = [
[z.number(), 5],
[z.object({ a: z.string(), b: z.bigint() }), { a: "ok", b: 4n }],
];
Expand Down
63 changes: 28 additions & 35 deletions src/parse-env.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
import * as z from "zod";

import { $ZodTypes, $ZodType, $ZodDefault } from "zod/v4/core";
import { getSchemaWithPreprocessor } from "./preprocessors.js";
import {
ErrorWithContext,
makeDefaultReporter,
Reporter,
TokenFormatters,
errorMap,
type TokenFormatters,
type ErrorWithContext,
type Reporter,
} from "./reporter.js";

import type * as z from "zod";
import type { DeepReadonlyObject } from "./util/type-helpers.js";

export type SimpleSchema<TOut = any, TIn = any> = z.ZodType<
TOut,
z.ZodTypeDef,
TIn
>;
export type SimpleSchema<Out = any, In = any> = $ZodType<Out, In>;

export type DetailedSpec<
TSchema extends SimpleSchema = SimpleSchema<unknown, unknown>,
> =
export type DetailedSpec<TSchema extends SimpleSchema = SimpleSchema> =
TSchema extends SimpleSchema<any, infer TIn>
? {
/**
Expand All @@ -31,6 +24,7 @@ export type DetailedSpec<
/**
* A description of this env var that's provided as help text if the
* passed value fails validation, or is required but missing.
* @deprecated use e.g. `z.string().meta({ description: 'Your description' })` instead
*/
description?: string;

Expand Down Expand Up @@ -124,15 +118,14 @@ export function parseEnvImpl<T extends Schemas & RestrictSchemas<T>>(
schemas: T,
reporterOrTokenFormatters: Reporter | TokenFormatters,
): DeepReadonlyObject<ParsedSchema<T>> {
const parsed: Record<string, unknown> = {} as DeepReadonlyObject<
ParsedSchema<T>
>;
const reporter =
typeof reporterOrTokenFormatters === "function"
? reporterOrTokenFormatters
: makeDefaultReporter(reporterOrTokenFormatters);

const parsed: Record<string, unknown> = {} as DeepReadonlyObject<
ParsedSchema<T>
>;

const errors: ErrorWithContext[] = [];

for (const [key, schemaOrSpec] of Object.entries(schemas)) {
Expand All @@ -141,10 +134,11 @@ export function parseEnvImpl<T extends Schemas & RestrictSchemas<T>>(
let defaultUsed = false;
let defaultValue: unknown;
try {
if (schemaOrSpec instanceof z.ZodType) {
if (envValue == null && schemaOrSpec instanceof z.ZodDefault) {
if (schemaOrSpec instanceof $ZodType) {
if (envValue == null && schemaOrSpec instanceof $ZodDefault) {
defaultUsed = true;
defaultValue = schemaOrSpec._def.defaultValue();
const spec = schemaOrSpec._zod;
defaultValue = spec.def.defaultValue;
// we "unwrap" the default value ourselves and pass it to the schema.
// in the very unlikely case that the value isn't stable AND
// validation fails, this ensures the default value we report is the
Expand All @@ -153,12 +147,13 @@ export function parseEnvImpl<T extends Schemas & RestrictSchemas<T>>(
// we invoked the default getter and got 0.7, and then ran the parser
// against a missing env var and it generated another default of 0.4,
// we'd report a default value that _should_ have passed.)
parsed[key] = schemaOrSpec.parse(defaultValue, { errorMap });
parsed[key] = (spec.def.innerType as z.ZodType).parse(defaultValue, {
error: errorMap,
});
} else {
parsed[key] = getSchemaWithPreprocessor(schemaOrSpec).parse(
envValue,
{ errorMap },
);
parsed[key] = getSchemaWithPreprocessor(
schemaOrSpec as $ZodTypes,
).parse(envValue, { error: errorMap });
}
} else if (envValue == null) {
[defaultUsed, defaultValue] = resolveDefaultValueForSpec(
Expand All @@ -167,22 +162,20 @@ export function parseEnvImpl<T extends Schemas & RestrictSchemas<T>>(
);

if (defaultUsed) {
parsed[key] = schemaOrSpec.schema.parse(defaultValue, { errorMap });
parsed[key] = (schemaOrSpec.schema as z.ZodType).parse(defaultValue);
} else {
// if there's no default, pass our envValue through the
// schema-with-preprocessor (it's an edge case, but our schema might
// accept `null`, and the preprocessor will convert `undefined` to
// `null` for us).
parsed[key] = getSchemaWithPreprocessor(schemaOrSpec.schema).parse(
envValue,
{ errorMap },
);
parsed[key] = getSchemaWithPreprocessor(
schemaOrSpec.schema as $ZodTypes,
).parse(envValue, { error: errorMap });
}
} else {
parsed[key] = getSchemaWithPreprocessor(schemaOrSpec.schema).parse(
envValue,
{ errorMap },
);
parsed[key] = getSchemaWithPreprocessor(
schemaOrSpec.schema as $ZodTypes,
).parse(envValue, { error: errorMap });
}
} catch (e) {
errors.push({
Expand Down
Loading