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, including Zod and
Valibot.
Defaults
Coercion is on by default (coerce: true). Zod 4.2+ uses
z.number() and z.boolean(), and Valibot uses v.number() and
v.boolean(); ArkEnv converts "8080" and "true" before the schema
runs.
import from "@arkenv/core";
export const = (
{
PORT: "number.port",
DEBUG: "boolean = false",
TAGS: "string[]",
},
{
: {
PORT: "8080",
DEBUG: "true",
TAGS: "web, api",
},
},
);
.PORT; // number
.DEBUG; // boolean
.TAGS; // string[]Numbers
PORT=3000 arrives as the string "3000". Parsing it here is what
makes port + 1 equal 3001.
Any field typed as number or a numeric subtype (including ArkType
number.port, Zod z.int(), or Valibot
v.number()) is parsed from the string form:
import from "@arkenv/core";
export const = (
{
PORT: "number.port",
RETRY_COUNT: "number.integer = 3",
AGE: "0 <= number.integer <= 120",
},
{
: {
PORT: "8080",
RETRY_COUNT: "5",
AGE: "30",
},
},
);
env.PORT;Booleans
boolean accepts the lowercase strings "true" and "false" only.
Values like "0", "1", "True", "YES", or "on" are not
coerced and fail validation. The same rule applies to ArkType
boolean, Zod z.boolean(), and Valibot v.boolean().
import from "@arkenv/core";
export const = (
{ DEBUG: "boolean = false" },
{ : { DEBUG: "true" } },
);
env.DEBUG;Need a wider truthy set? Normalize with a transform
(an ArkType morph, Zod .transform, or Valibot v.transform), or keep
the field as string and map it in application code.
Arrays
A list in an env file is still one string. Splitting on commas is how
TAGS=web, api becomes ["web", "api"].
By default ArkEnv splits on commas and trims each entry:
import from "@arkenv/core";
export const = (
{
TAGS: "string[]",
PORTS: "number[]",
},
{
: {
TAGS: "web, app, api",
PORTS: "3000, 8080",
},
},
);
env.TAGS;env.PORTS;Switch to JSON arrays with arrayFormat: "json":
import from "@arkenv/core";
export const = (
{ TAGS: "string[]" },
{
: "json",
: { TAGS: '["web", "app"]' },
},
);
env.TAGS;Objects
A nested config has to fit in one environment variable, so the usual shape is a JSON string. ArkEnv parses that string, then coerces nested fields the same way it coerces top-level ones.
import from "@arkenv/core";
export const = (
{
: {
HOST: "string",
PORT: "number",
},
},
{
: {
DATABASE: '{"HOST": "localhost", "PORT": "5432"}',
},
},
);
env.DATABASE;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").((value) => value.trim().toUpperCase()),
PORT: "number.port = 3000",
},
{ : { BUILD_ID: " abc " } },
);
.BUILD_ID; // "ABC"Coercion can feed a transformation. The string "3" becomes a number
before your pipe runs:
import , { } from "@arkenv/core";
export const = (
{
: ("number").((n) => Math.max(0, n)),
},
{ : { RETRY_COUNT: "3" } },
);
.RETRY_COUNT; // 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 = ({ PORT: .number(), DEBUG: .boolean() });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 { toJsonSchema } from "@valibot/to-json-schema";
export const = (
{ PORT: .number(), DEBUG: .boolean() },
{
: (schema) =>
toJsonSchema(schema 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 = (
{ : .(), : .() },
{
: (schema) =>
(schema 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 { toJsonSchema } from "@valibot/to-json-schema";
import * as from "valibot";
import * as from "zod/mini";
export const = (
{
PORT: .number(),
: .(),
},
{
: (schema) => {
switch (schema["~standard"].) {
case "valibot":
return toJsonSchema(schema as ., {
: "input",
: "draft-07",
});
case "zod":
return .(schema 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 = (
{ PORT: "string" },
{ : false, : { PORT: "3000" } },
);
.PORT; // stringWith coerce: false, a numeric schema rejects "3000" unless you
transform the string yourself (or use a helper such as
z.coerce.number() or Valibot v.pipe(v.string(), v.transform(Number))).