ZORL
Complete Guide to Type-Safe Environment Variables
8 min read

Complete Guide to Type-Safe Environment Variables

Learn how to implement type-safe environment variables using JSON schemas. Covers validation rules, schema inheritance, auto-documentation, and CI/CD integration.

type-safe environment variablesenv schema validationdotenv best practicesconfiguration managementzorath-env

Environment variables have no types. Every value is a string. Your code assumes PORT is a number, DEBUG is a boolean, and DATABASE_URL is a valid URL. These assumptions fail silently until production breaks.

Type-safe environment variables solve this problem. Define expected types in a schema. Validate before deployment. Catch configuration errors before they cause outages.

This guide covers everything you need to implement type-safe environment variables in any project.

Why Type Safety Matters for Configuration

Consider this .env file:

PORT=three-thousand
DEBUG=yes
DATABASE_URL=not-a-url
API_TIMEOUT=30.5

Every value is technically valid as a string. But your application expects:

  • PORT to be an integer
  • DEBUG to be a boolean
  • DATABASE_URL to be a valid URL
  • API_TIMEOUT to be a float

Without validation, these errors reach production. With type-safe schemas, they get caught immediately:

$ zenv check
zenv check failed:

- PORT: expected int, got 'three-thousand'
- DEBUG: expected bool, got 'yes'
- DATABASE_URL: expected url, got 'not-a-url'

Type safety transforms environment configuration from a source of bugs into a reliable foundation for your application.

Supported Types

A type-safe schema defines the expected format for each variable. zorath-env supports six types that cover most configuration needs.

String Type

The default type. Accepts any text value.

{
  "APP_NAME": {
    "type": "string",
    "required": true,
    "description": "Application display name"
  }
}

Integer Type

Whole numbers only. Rejects decimals and non-numeric values.

{
  "PORT": {
    "type": "int",
    "required": true,
    "default": 3000,
    "description": "HTTP server port"
  },
  "MAX_CONNECTIONS": {
    "type": "int",
    "default": 100
  }
}

Float Type

Decimal numbers. Useful for rates, percentages, and thresholds.

{
  "RATE_LIMIT": {
    "type": "float",
    "default": 100.0,
    "description": "Requests per second limit"
  },
  "CACHE_TTL_HOURS": {
    "type": "float",
    "default": 24.0
  }
}

Boolean Type

Accepts true, false, 1, 0, yes, no (case-insensitive). Normalizes inconsistent values across environments.

{
  "DEBUG": {
    "type": "bool",
    "default": false,
    "description": "Enable debug logging"
  },
  "FEATURE_ENABLED": {
    "type": "bool",
    "required": true
  }
}

URL Type

Validates URL structure. Catches malformed connection strings before they cause runtime errors.

{
  "DATABASE_URL": {
    "type": "url",
    "required": true,
    "description": "PostgreSQL connection string"
  },
  "REDIS_URL": {
    "type": "url",
    "required": true
  }
}

Enum Type

Restricts values to a predefined list. Prevents typos and invalid configurations.

{
  "NODE_ENV": {
    "type": "enum",
    "values": ["development", "staging", "production"],
    "required": true,
    "description": "Runtime environment"
  },
  "LOG_LEVEL": {
    "type": "enum",
    "values": ["debug", "info", "warn", "error"],
    "default": "info"
  }
}

See the full schema reference for detailed documentation on each type.

Validation Rules

Types catch format errors. Validation rules catch semantic errors. Combine them for comprehensive configuration validation.

Numeric Constraints

Set minimum and maximum bounds for integers and floats:

{
  "PORT": {
    "type": "int",
    "validate": {
      "min": 1024,
      "max": 65535
    },
    "description": "Must be a non-privileged port"
  },
  "CACHE_SIZE_MB": {
    "type": "float",
    "validate": {
      "min_value": 0.0,
      "max_value": 1024.0
    }
  }
}

String Length Constraints

Enforce minimum and maximum lengths for strings:

{
  "API_KEY": {
    "type": "string",
    "required": true,
    "validate": {
      "min_length": 32,
      "max_length": 64
    },
    "description": "API key must be 32-64 characters"
  },
  "SESSION_SECRET": {
    "type": "string",
    "validate": {
      "min_length": 64
    }
  }
}

Pattern Matching

Use regular expressions for complex validation:

{
  "EMAIL": {
    "type": "string",
    "validate": {
      "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
    }
  },
  "AWS_REGION": {
    "type": "string",
    "validate": {
      "pattern": "^[a-z]{2}-[a-z]+-\\d$"
    },
    "description": "AWS region format (e.g., us-east-1)"
  }
}

Validation rules work alongside type checking. A variable must pass both type validation and any custom rules to be considered valid.

Variable Interpolation

Complex configurations often reference other variables. Type-safe schemas support variable interpolation with ${VAR} syntax:

# .env
BASE_URL=https://api.example.com
API_V1_ENDPOINT=${BASE_URL}/v1
API_V2_ENDPOINT=${BASE_URL}/v2
WEBHOOK_URL=${BASE_URL}/webhooks/incoming

PORT=8080
HEALTH_CHECK_URL=http://localhost:${PORT}/health

Interpolation resolves before validation. The final values get type-checked against your schema.

Circular Reference Detection

Circular references cause infinite loops. zorath-env detects and reports them:

# .env
A=${B}
B=${C}
C=${A}  # Circular!
$ zenv check
zenv check failed:

- Circular reference detected: A -> B -> C -> A

This prevents runtime hangs from misconfigured variable chains.

Schema Inheritance

Different environments need different configurations. Schema inheritance lets you define a base schema and extend it for specific environments.

Base Schema

Create a base schema with common variables:

// base.schema.json
{
  "DATABASE_URL": {
    "type": "url",
    "required": true,
    "description": "Database connection string"
  },
  "LOG_LEVEL": {
    "type": "enum",
    "values": ["debug", "info", "warn", "error"],
    "default": "info"
  },
  "PORT": {
    "type": "int",
    "default": 3000
  }
}

Production Schema

Extend the base schema with production-specific requirements:

// production.schema.json
{
  "extends": "base.schema.json",
  "SENTRY_DSN": {
    "type": "url",
    "required": true,
    "description": "Error tracking endpoint"
  },
  "LOG_LEVEL": {
    "type": "enum",
    "values": ["info", "warn", "error"],
    "default": "warn"
  }
}

The production schema:

  • Inherits all variables from base.schema.json
  • Adds SENTRY_DSN as a production requirement
  • Overrides LOG_LEVEL to exclude debug level

Development Schema

Create a development-specific schema with relaxed requirements:

// development.schema.json
{
  "extends": "base.schema.json",
  "DEBUG": {
    "type": "bool",
    "default": true
  },
  "MOCK_EXTERNAL_SERVICES": {
    "type": "bool",
    "default": true
  }
}

Validate with the appropriate schema for each environment:

# Development
zenv check --schema development.schema.json

# Production
zenv check --schema production.schema.json

Schema inheritance keeps your configurations DRY while allowing environment-specific customization. See the GitHub wiki for more inheritance patterns.

Auto-Generated Documentation

Your schema contains everything needed for documentation: variable names, types, descriptions, defaults, and constraints. Generate documentation automatically instead of maintaining it manually.

Markdown Output

$ zenv docs --schema env.schema.json

Output:

# Environment Variables

## `DATABASE_URL`
- Type: `url`
- Required: `true`

PostgreSQL connection string

## `PORT`
- Type: `int`
- Required: `false`
- Default: `3000`

HTTP server port

Redirect to a file for your repository:

zenv docs > ENVIRONMENT.md

JSON Output

For tooling integration, generate JSON:

zenv docs --format json > schema-docs.json

Generate Example Files

Create .env.example files from your schema:

# Without defaults
zenv example > .env.example

# With default values included
zenv example --include-defaults > .env.example

Output:

# DATABASE_URL (url, required)
# PostgreSQL connection string
DATABASE_URL=

# PORT (int, optional)
# HTTP server port
# Default: 3000
PORT=3000

Documentation stays in sync with your schema automatically. Learn more in the CLI commands reference.

CI/CD Integration

Type-safe validation becomes powerful when integrated into your deployment pipeline. Catch configuration errors before they reach any environment.

GitHub Actions

Use the official GitHub Action for seamless integration:

- name: Validate environment configuration
  uses: zorl-engine/zorath-env/.github/actions/zenv-action@main
  with:
    schema: env.schema.json
    env-file: .env.example

The action downloads prebuilt binaries for fast execution. No Rust compilation required.

Pre-Commit Hooks

Validate on every commit:

#!/usr/bin/env bash
set -e

if [ -f "env.schema.json" ]; then
  if command -v zenv >/dev/null 2>&1; then
    zenv check || exit 1
  else
    echo "Warning: zenv not installed, skipping validation"
  fi
fi

Exit Codes

zorath-env uses standard exit codes for CI/CD integration:

| Exit Code | Meaning | |-----------|---------| | 0 | Validation passed | | 1 | Validation failed |

Any CI system can interpret these codes to pass or fail builds appropriately.

For detailed CI/CD setup instructions, see our pipeline setup guide.

Getting Started

Implementing type-safe environment variables takes five minutes:

1. Install zorath-env

# Via cargo
cargo install zorath-env

# Or download binary from GitHub releases
# https://github.com/zorl-engine/zorath-env/releases

Prebuilt binaries available for Linux, macOS (Intel and Apple Silicon), and Windows. No Rust installation required.

2. Generate a Schema

If you have an existing .env.example file:

zenv init --example .env.example

This creates env.schema.json with types inferred from your values.

3. Refine Your Schema

Add descriptions, validation rules, and adjust types:

{
  "DATABASE_URL": {
    "type": "url",
    "required": true,
    "description": "PostgreSQL connection string"
  },
  "PORT": {
    "type": "int",
    "default": 3000,
    "validate": { "min": 1024, "max": 65535 }
  }
}

4. Validate

zenv check

5. Integrate

Add validation to your CI/CD pipeline and pre-commit hooks.

Resources

Conclusion

Type-safe environment variables eliminate an entire category of production bugs. Define your schema once, validate everywhere, and catch configuration errors before they cause outages.

The investment is minimal: a JSON file and a single command. The return is reliable configuration across all environments.

Start with zorath-env today. Your future self debugging a production incident will thank you.

Share this article

Z

ZORL Team

Building developer tools that make configuration easier. Creators of zorath-env.

Previous
5 Environment Variable Mistakes That Break Production
Next
zorath-env v0.3.1: Shell Completions, GitHub Action, and Cross-Platform CI/CD

Related Articles

Never miss config bugs again

Use zorath-env to validate your environment variables before they cause production issues.