Coercion and parsing
Turn string environment variables into numbers, booleans, arrays, and objects.
Environment variables arrive as strings. ArkEnv coerces those strings
toward the types your schema declares so application code can treat
PORT as a number and DEBUG as a boolean.
The same coerce option and pre-coercion pipeline apply to
@arkenv/core and
@arkenv/standard.
Defaults
Coercion is on by default (coerce: true). Zod 4.2+ uses
z.number() and z.boolean() here; ArkEnv converts "8080" and
"true" before the schema runs.
import from "@arkenv/core";
export const = (
{
: "number.port",
: "boolean = false",
: "string[]",
},
{
: {
: "8080",
: "true",
: "web, api",
},
},
);
.; // number
.; // boolean
.; // string[]import from "@arkenv/standard";
import * as from "zod";
export const = (
{
: .().(0).(65535),
: .().(false),
: .(.()),
},
{
: {
: "8080",
: "true",
: "web, api",
},
},
);
.; // number
.; // boolean
.; // string[]Numbers
Any field typed as number or a numeric subtype (including
number.port) is parsed from the string
form:
import from "@arkenv/core";
export const = (
{
: "number.port",
: "number.integer = 3",
: "0 <= number.integer <= 120",
},
{
: {
: "8080",
: "5",
: "30",
},
},
);
env.;Booleans
boolean accepts the lowercase strings "true" and "false" only.
Values like "0", "1", "True", "YES", or "on" are not
coerced and fail validation.
import from "@arkenv/core";
export const = (
{ : "boolean = false" },
{ : { : "true" } },
);
env.;Need a wider truthy set? Normalize with a transform
(ArkType morph or Zod/Valibot .transform), or keep the field as
string and map it in application code.
Arrays
By default ArkEnv splits on commas and trims each entry:
import from "@arkenv/core";
export const = (
{
: "string[]",
: "number[]",
},
{
: {
: "web, app, api",
: "3000, 8080",
},
},
);
env.;env.;Switch to JSON arrays with arrayFormat: "json":
import from "@arkenv/core";
export const = (
{ : "string[]" },
{
: "json",
: { : '["web", "app"]' },
},
);
env.;Objects
JSON strings map onto nested object schemas. Nested fields are coerced too:
import from "@arkenv/core";
export const = (
{
: {
: "string",
: "number",
},
},
{
: {
: '{"HOST": "localhost", "PORT": "5432"}',
},
},
);
env.;Pre-coercion model
ArkEnv does not wrap your schema in a coercion pipe. It uses a shared pre-coercion pipeline for both engines:
- Introspect the schema's JSON Schema (ArkType
.in.toJsonSchema, or Standard Schema metadata / optionaltoJsonSchemafallback) - Find paths that need conversion (numbers, booleans, arrays, objects)
- Copy the input record and coerce only those paths on the copy
- Validate the copy with the original, unmodified schema
process.env is never mutated. Schema transforms and refinements still
see the coerced values.
Standard Schema ≠Standard JSON Schema
Automatic coercion with @arkenv/standard needs Standard JSON Schema
v1 on the value or a
converter. Those specs are orthogonal to Standard Schema validation. Zod
4.2+ and VineJS 4.3+ put the converter on the schema. Valibot and Zod Mini
use first-class subpaths (@arkenv/standard/valibot,
@arkenv/standard/zod-mini). Zod v3 typically uses zod-to-json-schema
through the toJsonSchema escape hatch. See
Valibot and
Zod.
Transforms
A transform reshapes a value after it is a valid input: trim a string, clamp a number, map an enum. Schema libraries call these transforms, morphs, or pipes. ArkEnv runs them as part of parsing.
PORT still uses built-in coercion, not a custom pipe:
import , { } from "@arkenv/core";
export const = (
{
: ("string").(() => .().()),
: "number.port = 3000",
},
{ : { : " abc " } },
);
.; // "ABC"import from "@arkenv/standard";
import * as from "zod";
export const = (
{
:
.()
.(() => .().()),
: .().(0).(65535).(3000),
},
{ : { : " abc " } },
);
.; // "ABC"Coercion can feed a transformation. The string "3" becomes a number
before your pipe runs:
import , { } from "@arkenv/core";
export const = (
{
: ("number").(() => .(0, )),
},
{ : { : "3" } },
);
.; // 3import from "@arkenv/standard";
import * as from "zod";
export const = (
{
: .().(() => .(0, )),
},
{ : { : "3" } },
);
.; // 3| Need | Prefer |
|---|---|
"3000" → number, "true" → boolean | Coercion (on by default) |
| Trim, normalize, clamp, map enums | Transform on the field schema |
| Multi-step pipelines | ArkType .pipe / Zod .pipe / Valibot pipe |
Valibot (toJsonSchema)
Prefer @arkenv/standard/valibot so @valibot/to-json-schema is bound
for you (typeMode: "input", target: "draft-07"):
import { } from "@arkenv/standard/valibot";
import * as from "valibot";
export const = ({ : .(), : .() });Install @valibot/to-json-schema yourself; it is an optional peer of
@arkenv/standard. Recipe:
Valibot.
The optional toJsonSchema callback on root @arkenv/standard remains
the escape hatch for custom converters. ArkEnv calls it when it can't
read JSON Schema from that key:
import from "@arkenv/standard";
import * as from "valibot";
import { } from "@valibot/to-json-schema";
export const = (
{ : .(), : .() },
{
: () =>
( as ., {
: "input",
: "draft-07",
}),
},
);target: "draft-07"matches the JSON Schema draft ArkEnv expects.typeMode: "input"coerces env strings toward the schema's input type. A bare{ toJsonSchema }function reference uses Valibot's defaulttypeMode: "ignore".- Valibot's converter does not accept Standard Schema, so assert
as v.GenericSchemaat the call.
Zod v3
Same escape hatch: Zod v3 validates via Standard Schema but omits
Standard JSON Schema on the value. Convert with
zod-to-json-schema
(works with zod@3 or Zod 4's zod/v3 export):
import from "@arkenv/standard";
import { } from "zod/v3";
import { } from "zod-to-json-schema";
export const = (
{ : .(), : .() },
{
: () =>
( as ., {
: "none",
}),
},
);Recipe: Zod.
Mixing with Zod Mini
Prefer @arkenv/standard/zod-mini for Mini-only maps. Mini has no
~standard.jsonSchema and no instance .toJSONSchema(). When you mix
Valibot and Mini on root @arkenv/standard, switch on
schema["~standard"].vendor and return undefined for anything else.
Mini reports vendor "zod". Classic Zod keys never reach the callback
at runtime.
import from "@arkenv/standard";
import { } from "@valibot/to-json-schema";
import * as from "valibot";
import * as from "zod/mini";
export const = (
{
: .(),
: .(),
},
{
: () => {
switch (["~standard"].) {
case "valibot":
return ( as ., {
: "input",
: "draft-07",
});
case "zod":
return .( as ., {
: "input",
: "draft-07",
});
default:
return ;
}
},
},
);Disabling coercion
Turn the pipeline off when a field must stay a string, or when you want the schema library to parse it.
import from "@arkenv/core";
export const = (
{ : "string" },
{ : false, : { : "3000" } },
);
.; // stringimport from "@arkenv/standard";
import * as from "zod";
export const = (
{ : .() },
{ : false, : { : "3000" } },
);
.; // stringWith coerce: false, a numeric schema rejects "3000" unless you
transform the string yourself (or use a helper such as
z.coerce.number()).