jsontoschema
All posts
typescriptconfigtypesworkflow

Type-Safe Config Files from JSON Samples

Most projects have at least one JSON config file that gets read at runtime with zero type checking. A database config, a feature flags file, a service manifest. The shape lives in the heads of whoever wrote it, and the code that reads it uses any or a hand-written interface that drifted from reality three months ago.

You can fix this in under a minute by generating a TypeScript interface from the actual file.

The problem with hand-written config types

Consider a deployment config:

{
  "service": "api-gateway",
  "port": 8080,
  "replicas": 3,
  "healthCheck": {
    "path": "/health",
    "intervalSeconds": 30,
    "timeout": 5
  },
  "features": {
    "rateLimiting": true,
    "cors": false,
    "logging": {
      "level": "info",
      "structured": true
    }
  }
}

A developer writes a TypeScript interface for this by hand. They forget timeout under healthCheck. They type interval instead of intervalSeconds. The compiler says nothing because the config is loaded with JSON.parse() and cast to any. The bug ships.

Generating the interface

Paste that JSON into the converter in TypeScript mode, or pipe it through the CLI:

cat deploy-config.json | npx @maisondigital/jsontoschema --typescript

Output:

interface HealthCheck {
  path: string;
  intervalSeconds: number;
  timeout: number;
}

interface Logging {
  level: string;
  structured: boolean;
}

interface Features {
  rateLimiting: boolean;
  cors: boolean;
  logging: Logging;
}

interface Root {
  service: string;
  port: number;
  replicas: number;
  healthCheck: HealthCheck;
  features: Features;
}

Every field is accounted for. Nested objects get their own named interfaces. Copy this into your codebase and use it where you load the config:

import config from "./deploy-config.json";
const typed: Root = config;

Now typed.healthCheck.intervalSeconds autocompletes, and typed.healthCheck.interval throws a compile error.

Keeping types in sync

Config files change. Fields get added, renamed, removed. The generated interface is a snapshot. When the config changes, regenerate:

cat deploy-config.json | npx @maisondigital/jsontoschema --typescript > config.types.ts

Add this to a pre-commit hook or CI step. If someone adds a field to the JSON but forgets to update the types, the next regeneration catches it. If someone updates the types but not the JSON, the mismatch surfaces at compile time.

Where this helps most

Config files are the obvious case, but the same pattern works for any JSON that your code treats as a known shape:

  • Feature flag definitions loaded from a file or API
  • Translation key manifests
  • Build tool configuration (custom fields in package.json)
  • Mock data files used in tests

Each of these is a place where a missing field or wrong type causes a runtime error that a generated interface would have caught at compile time.

One less thing to maintain by hand

Hand-writing interfaces for JSON you already have is busywork that introduces drift. Generate the types from the source of truth, keep them updated, and let the compiler do the checking. Try it with the converter.