Coercion and parsing

Turn string environment variables into numbers, booleans, arrays, and objects.

Edit on GitHub

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.

./env.ts
import  from "@arkenv/core";

export const  = (
  {
    : "number.port",
    : "boolean = false",
    : "string[]",
  },
  {
    : {
      : "8080",
      : "true",
      : "web, api",
    },
  },
);

.; // number
.; // boolean
.; // string[]
./env.ts
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:

./env.ts
import  from "@arkenv/core";

export const  = (
  {
    : "number.port",
    : "number.integer = 3",
    : "0 <= number.integer <= 120",
  },
  {
    : {
      : "8080",
      : "5",
      : "30",
    },
  },
);

env.;
const env: {
    PORT: number;
    RETRY_COUNT: number;
    AGE: number;
}

Booleans

boolean accepts the lowercase strings "true" and "false" only. Values like "0", "1", "True", "YES", or "on" are not coerced and fail validation.

./env.ts
import  from "@arkenv/core";

export const  = (
  { : "boolean = false" },
  { : { : "true" } },
);

env.;
const env: {
    DEBUG: boolean;
}

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:

./env.ts
import  from "@arkenv/core";

export const  = (
  {
    : "string[]",
    : "number[]",
  },
  {
    : {
      : "web, app, api",
      : "3000, 8080",
    },
  },
);

env.;
const env: {
    TAGS: string[];
    PORTS: number[];
}
env.;
const env: {
    TAGS: string[];
    PORTS: number[];
}

Switch to JSON arrays with arrayFormat: "json":

./env.ts
import  from "@arkenv/core";

export const  = (
  { : "string[]" },
  {
    : "json",
    : { : '["web", "app"]' },
  },
);

env.;
const env: {
    TAGS: string[];
}

Objects

JSON strings map onto nested object schemas. Nested fields are coerced too:

./env.ts
import  from "@arkenv/core";

export const  = (
  {
    : {
      : "string",
      : "number",
    },
  },
  {
    : {
      : '{"HOST": "localhost", "PORT": "5432"}',
    },
  },
);

env.;
const env: {
    DATABASE: {
        HOST: string;
        PORT: number;
    };
}

Pre-coercion model

ArkEnv does not wrap your schema in a coercion pipe. It uses a shared pre-coercion pipeline for both engines:

  1. Introspect the schema's JSON Schema (ArkType .in.toJsonSchema, or Standard Schema metadata / optional toJsonSchema fallback)
  2. Find paths that need conversion (numbers, booleans, arrays, objects)
  3. Copy the input record and coerce only those paths on the copy
  4. 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:

./env.ts
import , {  } from "@arkenv/core";

export const  = (
  {
    : ("string").(() => .().()),
    : "number.port = 3000",
  },
  { : { : "  abc  " } },
);

.; // "ABC"
./env.ts
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:

./env.ts
import , {  } from "@arkenv/core";

export const  = (
  {
    : ("number").(() => .(0, )),
  },
  { : { : "3" } },
);

.; // 3
./env.ts
import  from "@arkenv/standard";
import * as  from "zod";

export const  = (
  {
    : .().(() => .(0, )),
  },
  { : { : "3" } },
);

.; // 3
NeedPrefer
"3000" → number, "true" → booleanCoercion (on by default)
Trim, normalize, clamp, map enumsTransform on the field schema
Multi-step pipelinesArkType .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"):

./env.ts
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:

./env.ts
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 default typeMode: "ignore".
  • Valibot's converter does not accept Standard Schema, so assert as v.GenericSchema at 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):

./env.ts
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.

./env.ts
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.

./env.ts
import  from "@arkenv/core";

export const  = (
  { : "string" },
  { : false, : { : "3000" } },
);

.; // string
./env.ts
import  from "@arkenv/standard";
import * as  from "zod";

export const  = (
  { : .() },
  { : false, : { : "3000" } },
);

.; // string

With coerce: false, a numeric schema rejects "3000" unless you transform the string yourself (or use a helper such as z.coerce.number()).

Next steps