---
title: Validation
description: Schema definition, environment parsing, error handling, dotenv support, and projection utilities.
url: https://pr-1-ee19382a0710.thally.app/envlock/validation
---

# Validation

Schema definition, environment parsing, error handling, dotenv support, and projection utilities.

This page covers everything from defining a schema to parsing environment sources, handling errors, working with dotenv files, and using projection utilities for documentation, diffing, and redaction.

## Schema definition

### `defineEnv(shape)`

Creates an `EnvSchema` from a plain object whose keys are variable names and whose values are fields built with the `env` namespace.

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

const schema = defineEnv({
  PORT: env.port().default(3000),
  DATABASE_URL: env.url().secret(),
});
```

Variable names must match the pattern `[A-Za-z_][A-Za-z0-9_]*` -- letters, digits, and underscores, not starting with a digit. A `TypeError` is thrown for invalid names:

```
Invalid environment variable name "2FAST": use letters, digits and underscores, not starting with a digit
```

The returned schema is frozen and preserves declaration order. Its shape is:

```ts
interface EnvSchema<Shape extends Record<string, AnyField> = Record<string, AnyField>> {
  readonly kind: "envlock.schema";
  readonly shape: Shape;
  readonly keys: readonly string[];   // declaration order
}
```

The `isEnvSchema(value)` type guard checks whether a value is an `EnvSchema` by inspecting its `kind`, `keys`, and `shape` properties.

## Parsing and validation

### `parseEnv(schema, source, options?)`

Validates a source record against a schema and returns a `ParseResult`. This function never throws -- it collects all issues before returning.

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

Where `EnvSource` is `Readonly<Record<string, string | undefined>>` (such as `process.env`).

The result is a discriminated union:

```ts
type ParseResult<S extends EnvSchema> =
  | { readonly ok: true; readonly values: Infer<S>; readonly issues: readonly [] }
  | { readonly ok: false; readonly issues: readonly EnvIssue[] };
```

**Parsing rules:**

- Variables are checked in declaration order.
- Both `undefined` and `""` (empty string) are treated as absent.
- Absent with `.default()` -- the default value is used.
- Absent with `.optional()` -- the field is skipped (no issue, no value).
- Absent otherwise -- a `missing` issue is recorded.
- Present -- the field's `parse` function runs. On failure, an `invalid` issue is recorded.

**Strict mode:**

Pass `{ strict: true }` to report source keys that are not declared in the schema as `unknown` issues. This is most useful with bounded sources such as dotenv files rather than `process.env`.

```js
import { parseEnv } from "@envlock/core";

const result = parseEnv(schema, dotenvRecord, { strict: true });
```

Issues for declared keys appear first (in declaration order), followed by unknown keys sorted alphabetically.

### `loadEnv(schema, source?, options?)`

A convenience wrapper that returns the typed values directly or throws an `EnvValidationError` on failure. When `source` is omitted, it defaults to `process.env`.

```js
import { loadEnv } from "@envlock/core";
import schema from "./envlock.config.mjs";

// Throws EnvValidationError listing every problem if validation fails.
const config = loadEnv(schema);
```

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

## Issue codes

Every validation issue has one of three codes:

| Code | Meaning |
|------|---------|
| `missing` | A required variable is not set or is empty |
| `invalid` | A variable is set but its value does not satisfy the declared type |
| `unknown` | A variable exists in the source but is not declared in the schema (strict mode only) |

The `EnvIssue` interface:

```ts
interface EnvIssue {
  readonly key: string;
  readonly code: IssueCode;       // "missing" | "invalid" | "unknown"
  readonly message: string;
  readonly received?: string;     // masked as "••••••" for secret fields
}
```

Missing issues use one of two messages depending on the source value:

- `"required variable is not set"` -- when the key is `undefined`
- `"required variable is set but empty"` -- when the key is `""`

Unknown issues use the message `"variable is not declared in the contract"`.

The `ISSUE_CODES` constant provides the three code strings for programmatic use:

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

## Error formatting

### `formatIssues(issues)`

Formats an array of issues as an indented list, one line per issue:

```
  - PORT: expected a port number between 1 and 65535 (received "abc")
  - DATABASE_URL: required variable is not set
```

### `EnvValidationError`

Thrown by `loadEnv` when validation fails. Extends `Error` with an `issues` array.

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

The error message follows this format:

```
Environment validation failed (2 issues):
  - PORT: expected a port number between 1 and 65535 (received "abc")
  - DATABASE_URL: required variable is not set
```

## Dotenv support

### `parseDotenv(text)`

Parses a `.env` file string into a plain record. Never throws -- malformed lines are silently skipped.

```ts
const parseDotenv: (text: string) => Record<string, string>;
```

Supported syntax:

- `KEY=value` -- plain assignment
- `export KEY=value` -- export prefix
- `# comment` -- full-line comments
- `KEY=` -- empty value
- Single-quoted values (`KEY='literal'`) -- no escape processing
- Double-quoted values (`KEY="with\nnewline"`) -- supports `\n`, `\r`, `\t`, `\"`, `\\`, and `\$` escapes
- Multi-line quoted values
- Inline comments after unquoted values (space then `#`)

CRLF is normalized to LF. Later duplicate keys overwrite earlier ones. Key names must match `[A-Za-z_][A-Za-z0-9_]*`.

### `formatDotenv(record)`

Converts a record back into `.env` file text. Values that contain whitespace, `#`, quotes, backslashes, `$`, or newlines are automatically quoted. An empty record produces an empty string; a non-empty record ends with a trailing newline.

```ts
const formatDotenv: (record: Readonly<Record<string, string>>) => string;
```

Round-trip safe: `parseDotenv(formatDotenv(record))` equals `record`.

## Projections

### `renderExample(schema, options?)`

Generates `.env.example` text from a schema, including descriptions, type annotations, and placeholder values.

```js
import { renderExample } from "@envlock/core";
import schema from "./envlock.config.mjs";

const text = renderExample(schema);
```

Each variable block includes a description comment (if set), a metadata comment with the type and constraints, and the variable assignment with a placeholder value. Secret fields always have empty placeholders. The `exampleValue` is used when available, otherwise the stringified default, otherwise empty.

Options:

```ts
interface RenderExampleOptions {
  readonly header?: readonly string[];  // comment lines; [] to omit the header
}
```

The default header is:

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

### `diffEnv(schema, source)`

Returns a structured diff between a schema and an environment source, partitioned into missing, unknown, and invalid keys.

```ts
interface EnvDiff {
  readonly missing: readonly string[];     // required keys not provided
  readonly unknown: readonly string[];     // source keys not in schema (sorted)
  readonly invalid: readonly EnvIssue[];   // declared keys with parse failures
  readonly ok: boolean;                    // true when all three are empty
}

const diffEnv: (schema: EnvSchema, source: EnvSource) => EnvDiff;
```

Internally calls `parseEnv` with strict mode enabled, then partitions the issues by code.

### `redact(values, schema)`

Returns a shallow copy of a values object where every field marked `.secret()` is replaced with `"••••••"`. The input is not mutated.

```js
import { loadEnv, redact } from "@envlock/core";
import schema from "./envlock.config.mjs";

const config = loadEnv(schema);
console.log(redact(config, schema));
// DATABASE_URL and SESSION_SECRET are masked; other fields appear as-is.
```

The `REDACTED_VALUE` constant holds the mask string `"••••••"`.

### `describeSchema(schema)`

Returns a structured description of every field in the schema, in declaration order.

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

const describeSchema: (schema: EnvSchema) => SchemaDescription[];
```

Secret field defaults are shown as the redacted value. Secret field examples are omitted.

## Type inference

The `Infer` mapped type extracts a fully typed configuration object from a schema. Required fields (those without `.optional()`) and defaulted fields appear as required properties. Optional fields appear as optional properties.

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

const schema = defineEnv({
  PORT: env.port().default(3000),
  DEBUG: env.boolean().optional(),
  API_KEY: env.string().secret(),
});

type Config = Infer<typeof schema>;
// Equivalent to:
// {
//   readonly PORT: number;
//   readonly API_KEY: string;
//   readonly DEBUG?: boolean;
// }
```

The type definition:

```ts
type Infer<S extends EnvSchema> = {
  readonly [K in RequiredKeys<S["shape"]>]: FieldValue<S["shape"][K]>
} & {
  readonly [K in OptionalKeys<S["shape"]>]?: FieldValue<S["shape"][K]>
};
```

Where `FieldValue<F>` extracts the value type `T` from a `Field<T, boolean>`. Required keys are fields with `Required = true` (explicitly required or defaulted). Optional keys are fields with `Required = false` (marked with `.optional()`).