---
title: Validation and Parsing
description: Complete reference for validation functions, error types, and projection utilities in @envlock/core.
url: https://pr-1-ff84656b4b8a.thally.app/envlock/validation
---

# Validation and Parsing

Complete reference for validation functions, error types, and projection utilities in @envlock/core.

Envlock validates environment variables in two ways: `loadEnv` throws on failure
and returns typed values on success, while `parseEnv` returns a result object
you can inspect without catching exceptions. Both rely on the same underlying
logic. This page covers both functions, the error and issue types they produce,
and the projection utilities that build on top of them.

## loadEnv

```ts
function loadEnv<S extends EnvSchema>(
  schema: S,
  source?: EnvSource,
  options?: ParseOptions,
): Infer<S>
```

Validates `source` against `schema` and returns a fully typed object. If any
issues are found, it throws an `EnvValidationError`.

- `source` defaults to `process.env` when omitted.
- `options` accepts `{ strict?: boolean }`.

```ts
import { defineEnv, env, loadEnv } from "@envlock/core";

const schema = defineEnv({
  PORT: env.port().default(3000),
  DATABASE_URL: env.url().secret(),
  NODE_ENV: env.enum(["development", "production"]).default("development"),
});

// Validate process.env (throws on failure)
const config = loadEnv(schema);

console.log(config.PORT);        // number
console.log(config.DATABASE_URL); // string
console.log(config.NODE_ENV);     // "development" | "production"
```

To validate a custom source instead of `process.env`:

```ts
const config = loadEnv(schema, {
  PORT: "8080",
  DATABASE_URL: "postgres://localhost/mydb",
});
```

## parseEnv

```ts
function parseEnv<S extends EnvSchema>(
  schema: S,
  source: EnvSource,
  options?: ParseOptions,
): ParseResult<S>
```

Non-throwing alternative to `loadEnv`. Returns a `ParseResult<S>` discriminated
union:

- On success: `{ ok: true, values: Infer<S>, issues: readonly [] }`
- On failure: `{ ok: false, issues: readonly EnvIssue[] }`

```ts
import { defineEnv, env, parseEnv } from "@envlock/core";

const schema = defineEnv({
  API_KEY: env.string().secret(),
  TIMEOUT: env.duration().default(5000),
});

const result = parseEnv(schema, process.env);

if (result.ok) {
  console.log("Validated:", result.values.API_KEY);
} else {
  console.error("Issues found:");
  for (const issue of result.issues) {
    console.error(`  ${issue.key}: ${issue.message}`);
  }
}
```

### ParseOptions

```ts
type ParseOptions = { readonly strict?: boolean }
```

When `strict` is `true`, keys present in `source` but absent from the schema
are reported as `unknown` issues. Unknown keys are sorted alphabetically in the
issue list.

### EnvSource

```ts
type EnvSource = Readonly<Record<string, string | undefined>>
```

Any object with string keys and string-or-undefined values, including
`process.env`.

## Validation semantics

Both `loadEnv` and `parseEnv` follow the same rules when examining each key in
the schema:

1. **Absent values**: both `undefined` and empty string (`""`) count as absent.
2. **Absent + `.default()`**: the default value is used. No issue is produced.
3. **Absent + `.optional()`**: the key is omitted from the output object. No
   issue is produced.
4. **Absent + required**: a `missing` issue is produced. The message depends on
   the source value:
   - `undefined` produces `"required variable is not set"`.
   - Empty string produces `"required variable is set but empty"`.
5. **Present values**: the raw string is passed through the field's parser. If
   parsing fails, an `invalid` issue is produced with the parser's error
   message.
6. **Strict mode**: when `strict: true`, any key in `source` that is not
   declared in the schema produces an `unknown` issue with the message
   `"variable is not declared in the contract"`.

**Issue ordering**: issues for declared schema keys appear first, in declaration
order (the order fields were listed in `defineEnv`). Unknown key issues follow,
sorted alphabetically.

## EnvValidationError

```ts
class EnvValidationError extends Error {
  readonly issues: readonly EnvIssue[];
  name: "EnvValidationError";
}
```

Thrown by `loadEnv` when validation fails. The `name` property is set to
`"EnvValidationError"`.

The `message` follows this format:

```
Environment validation failed (N issue(s)):
  - KEY: message (received "value")
  - KEY2: message
```

The `(received "value")` suffix appears only for `invalid` issues that carry a
`received` field. Secret fields show `"received"` as the `REDACTED_VALUE`
(`"••••••"`).

## EnvIssue

```ts
interface EnvIssue {
  readonly key: string;
  readonly code: IssueCode;
  readonly message: string;
  readonly received?: string;
}
```

Each issue identifies the variable `key`, a machine-readable `code`, a
human-readable `message`, and an optional `received` value for `invalid` issues.
For secret fields, `received` is masked as `REDACTED_VALUE`.

### IssueCode

```ts
type IssueCode = "missing" | "invalid" | "unknown"
```

### ISSUE_CODES

```ts
const ISSUE_CODES = {
  missing: "missing",
  invalid: "invalid",
  unknown: "unknown",
}
```

### REDACTED_VALUE

```ts
const REDACTED_VALUE = "••••••"
```

Six bullet characters. Used whenever a secret field's value would otherwise
appear in issues, examples, schema descriptions, or redacted output.

## formatIssues

```ts
function formatIssues(issues: readonly EnvIssue[]): string
```

Produces an indented bullet list from an array of issues. Each line has the
form:

```
  - KEY: message (received "value")
```

The `(received "value")` suffix is included only when the issue has a `received`
field.

## Projection functions

These utilities build on `parseEnv` and `describeSchema` to support common
workflows like masking secrets, generating example files, and detecting drift.

### redact

```ts
function redact<T extends Readonly<Record<string, unknown>>>(
  values: T,
  schema: EnvSchema,
): { readonly [K in keyof T]: T[K] | string }
```

Returns a shallow copy of `values` with every field marked `.secret()` in the
schema replaced by `REDACTED_VALUE` (`"••••••"`). Undefined values are left
as-is. The input object is not mutated.

```ts
import { loadEnv, redact } from "@envlock/core";

const config = loadEnv(schema);
console.log(redact(config, schema));
// { PORT: 3000, DATABASE_URL: "••••••", NODE_ENV: "development" }
```

### describeSchema

```ts
function describeSchema(schema: EnvSchema): SchemaDescription[]
```

Returns a JSON-serializable array describing each variable in the schema, in
declaration order.

```ts
interface SchemaDescription {
  readonly key: string;
  readonly type: FieldKind;
  readonly required: boolean;
  readonly hasDefault: boolean;
  readonly default?: unknown;
  readonly secret: boolean;
  readonly description?: string;
  readonly example?: string;
  readonly constraints?: string;
}
```

- `required` is `true` when the field is not optional and has no default.
- Secret defaults are masked as `REDACTED_VALUE`.
- Secret example values are omitted.

### diffEnv

```ts
function diffEnv(schema: EnvSchema, source: EnvSource): EnvDiff
```

Compares `source` against `schema` and partitions the results into categories.
Internally runs `parseEnv` with `strict: true`.

```ts
interface EnvDiff {
  readonly missing: readonly string[];
  readonly unknown: readonly string[];
  readonly invalid: readonly EnvIssue[];
  readonly ok: boolean;
}
```

- `missing`: keys required by the schema but absent from the source.
- `unknown`: keys present in the source but not declared in the schema.
- `invalid`: issues for keys whose values failed parsing.
- `ok`: `true` when all three lists are empty.

### renderExample

```ts
function renderExample(
  schema: EnvSchema,
  options?: RenderExampleOptions,
): string
```

Renders `.env.example` text from the schema.

```ts
interface RenderExampleOptions {
  readonly header?: readonly string[];
}
```

**Header**: by default, the output starts with:

```
# Environment contract rendered by envlock.
# Copy to .env and fill in the values; never commit real secrets here.
```

Pass a `header` array to replace it with custom comment lines (each entry
becomes a `# ...` line). Pass an empty array (`[]`) to omit the header entirely.

**Per-variable blocks** follow this layout:

```
# <description>
# <type> . <required|optional> . <default: X> . <constraints> . secret
KEY=<placeholder>
```

The description line appears only when the field has a `.describe()` value. The
metadata line lists applicable attributes separated by ` . `.

**Placeholder logic**:

- Secret fields always get a blank value (`KEY=`).
- Fields with `.example()` use that value.
- Fields with `.default()` stringify the default (strings verbatim, arrays
  joined by `,`, durations with `ms` suffix, otherwise `JSON.stringify`).
- Otherwise the value is blank.

Secret defaults are displayed as `(hidden)` in the metadata comment line.