Pkl Tutorial: Apple's Type-Safe Configuration Language (2026)

YAML is ubiquitous but fragile. JSON is strict but verbose. Pkl gives you type-safe, programmable configuration that generates both—and catches errors before deployment. Here is a practical guide to getting started.

In This Guide
  1. What Is Pkl and Why Apple Built It
  2. Pkl vs YAML vs JSON vs TOML
  3. Installing Pkl (CLI, IDE Plugins)
  4. Pkl Syntax Basics: Types, Classes, Objects
  5. Templates and Inheritance
  6. Generating YAML, JSON, and Property Files
  7. Integrating with Kubernetes and Docker
  8. IDE Support: VS Code, IntelliJ, Neovim
  9. Migration from YAML to Pkl
  10. When to Use Pkl (and When Not To)
  11. Related Developer Tools
  12. Frequently Asked Questions

Configuration files are the connective tissue of modern software. They wire up databases, define infrastructure, control feature flags, and shape deployment pipelines. Yet the formats most teams rely on—YAML, JSON, TOML—were never designed for the complexity they now carry. A misplaced indent in a Kubernetes manifest can take down a production cluster. A typo in a JSON config file produces no error until runtime.

Pkl (pronounced "pickle") is Apple's answer to this problem. Released as open source in February 2024 under the Apache 2.0 license, Pkl is a configuration-as-code language with a type system, classes, validation constraints, and the ability to generate output in JSON, YAML, XML, and Java Properties formats. You write configuration once in Pkl, and it produces whatever format your tools expect—with type errors caught at evaluation time, not in production.

Working with config output? The YAML Formatter and JSON Formatter let you validate and clean up generated output directly in your browser.

What Is Pkl and Why Apple Built It

Apple manages configuration at enormous scale—across iOS, macOS, server infrastructure, and internal tooling. The company needed a language that could express configuration with the same rigor they apply to application code: type safety, reusability, and validation. Static formats like YAML and JSON lack all three.

Pkl sits between static configuration formats and general-purpose programming languages. It is declarative enough to read like a config file but programmable enough to eliminate the copy-paste duplication that plagues large YAML codebases. The key properties that define Pkl:

The language is written in Kotlin and ships as native binaries for macOS, Linux, and Windows, with no JVM required for the native builds. It also provides code generation for Java, Kotlin, Swift, and Go, enabling type-safe configuration consumption in application code.

Pkl vs YAML vs JSON vs TOML

Before committing to a new configuration tool, you need to understand what each format does well and where it falls short. Here is a direct comparison.

Feature Pkl YAML JSON TOML
Type system Full (String, Int, Float, Boolean, Duration, DataSize, custom classes) Implicit (parser-dependent coercion) Basic (string, number, boolean, null, array, object) Moderate (string, integer, float, boolean, datetime, array, table)
Validation Built-in constraints on types External (JSON Schema, kubeconform) External (JSON Schema) None built-in
Comments Line and block comments Line comments Not supported Line comments
Templating / Reuse Classes, modules, inheritance, amending YAML anchors (limited) None None
Multi-line strings Yes, with string interpolation Yes (block scalars) No Yes (literal strings)
Output formats JSON, YAML, XML, Properties, Pkl YAML only JSON only TOML only
IDE support VS Code, IntelliJ, Neovim (LSP) Broad Broad Moderate
Learning curve Moderate (new syntax, but familiar concepts) Low (but indentation is error-prone) Very low Low
Toolchain requirement Pkl CLI required None (text files) None (text files) None (text files)

The trade-off is clear: Pkl adds a build step and a learning curve in exchange for type safety, reusability, and multi-format output. For simple, one-off configs, YAML or TOML remain the pragmatic choice. For large-scale configuration that is shared across teams, environments, or services, Pkl pays for itself by catching errors early and reducing duplication.

If you need to convert between formats during evaluation, the YAML to JSON Converter handles the transformation instantly for quick spot checks.

Installing Pkl (CLI, IDE Plugins)

Pkl provides native executables that start instantly and run without a JVM. Choose the method that fits your system.

macOS and Linux (Homebrew)

Terminal
brew install pkl
pkl --version

Windows (winget)

PowerShell
winget install Apple.Pkl
pkl --version

Cross-Platform (mise)

Terminal
mise use -g pkl
pkl --version

Direct Download

Download native binaries from the GitHub releases page. Builds are available for macOS (amd64, aarch64), Linux (amd64, aarch64, Alpine), and Windows.

Verify the installation

After installing, run pkl eval -e "greeting = \"Hello, Pkl!\"" in your terminal. You should see greeting = Hello, Pkl! as output.

Pkl Syntax Basics: Types, Classes, Objects

Pkl files use the .pkl extension. The syntax is clean and indentation-independent—blocks are delimited by curly braces, not whitespace.

Properties and Types

config.pkl
name: String = "my-api"
port: Int = 8080
debug: Boolean = false
timeout: Duration = 30.s
maxUpload: DataSize = 50.mb

Pkl has built-in types for common configuration values that YAML and JSON treat as plain strings: Duration (with units like .s, .min, .h) and DataSize (with units like .kb, .mb, .gb). These are type-checked, so timeout: Duration = "thirty" produces a compile-time error.

Classes

Classes define the shape of your configuration. Think of them as schemas that are enforced at evaluation time.

server.pkl
class Server {
  host: String
  port: Int(isBetween(1, 65535))
  tls: Boolean = true
  workers: Int(isPositive) = 4
}

class Database {
  url: String
  pool: Int(isBetween(1, 100)) = 10
  timeout: Duration = 5.s
}

class AppConfig {
  name: String
  version: String
  server: Server
  database: Database
}

Objects

Create instances of classes using the new keyword.

production.pkl
import "server.pkl"

config: AppConfig = new {
  name = "payment-api"
  version = "2.4.1"
  server = new {
    host = "0.0.0.0"
    port = 443
    tls = true
    workers = 8
  }
  database = new {
    url = "postgresql://db.internal:5432/payments"
    pool = 25
    timeout = 10.s
  }
}

String Interpolation

Pkl uses \(expression) for string interpolation, similar to Swift.

Pkl
name = "api-gateway"
port = 8080
healthCheck = "http://localhost:\(port)/\(name)/health"
// Result: "http://localhost:8080/api-gateway/health"

Type Constraints

Constraints are expressions attached to type annotations. They run at evaluation time and produce clear error messages when violated.

Pkl
class NetworkConfig {
  port: Int(isBetween(1, 65535))
  maxConnections: Int(isPositive)
  hostname: String(!isEmpty)
  protocol: "http"|"https"       // Union type: only these values allowed
  retries: Int(this >= 0 && this <= 10)
}

If you assign port = 70000, Pkl outputs: Expected value to be between 1 and 65535, but got 70000. This is the kind of error that YAML catches never and JSON Schema catches only if you have written and maintained a separate schema file.

Templates and Inheritance

One of Pkl's strongest advantages over static formats is eliminating configuration duplication through templates and amending.

Base Template

Define a base configuration that other files can extend.

base-service.pkl
class Service {
  name: String
  replicas: Int = 2
  port: Int = 8080
  healthPath: String = "/health"
  env: Mapping<String, String> = new {}
}

service: Service = new {
  name = "default"
  env = new {
    ["LOG_LEVEL"] = "info"
    ["NODE_ENV"] = "production"
  }
}

Amending for Environments

The amends keyword creates a new configuration that inherits from a base and overrides specific values. Unamended properties keep their original values.

staging.pkl
amends "base-service.pkl"

service {
  name = "user-service"
  replicas = 1
  env {
    ["LOG_LEVEL"] = "debug"
    ["NODE_ENV"] = "staging"
  }
}
production.pkl
amends "base-service.pkl"

service {
  name = "user-service"
  replicas = 6
  env {
    ["LOG_LEVEL"] = "warn"
    ["SENTRY_DSN"] = "https://key@sentry.io/123"
  }
}

In YAML, you would duplicate the entire configuration block for each environment and manually keep them in sync. In Pkl, the base template is the single source of truth, and each environment file declares only what differs.

Amend vs Extend

amends creates a modified copy of the base module. extends creates a subtype that inherits the base module's classes and definitions. Use amends for environment-specific configs. Use extends when building reusable module libraries.

Generating YAML, JSON, and Property Files

Pkl's multi-format output is what makes it practical for existing toolchains. Your CI/CD pipeline, Kubernetes, Docker, or application runtime does not need to know Pkl exists.

Generate YAML

Terminal
pkl eval -f yaml production.pkl
Output (YAML)
service:
  name: user-service
  replicas: 6
  port: 8080
  healthPath: /health
  env:
    LOG_LEVEL: warn
    NODE_ENV: production
    SENTRY_DSN: https://key@sentry.io/123

Generate JSON

Terminal
pkl eval -f json production.pkl
Output (JSON)
{
  "service": {
    "name": "user-service",
    "replicas": 6,
    "port": 8080,
    "healthPath": "/health",
    "env": {
      "LOG_LEVEL": "warn",
      "NODE_ENV": "production",
      "SENTRY_DSN": "https://key@sentry.io/123"
    }
  }
}

Generate Java Properties

Terminal
pkl eval -f properties production.pkl

Write Output to Files

Terminal
# Write YAML to a file
pkl eval -f yaml -o config.yaml production.pkl

# Write JSON to a file
pkl eval -f json -o config.json production.pkl

# Evaluate multiple files at once
pkl eval -f yaml -m output/ staging.pkl production.pkl

The -m flag writes multiple output files, one per input module. This is useful for generating all environment configs in a single command.

After generating output, verify the structure with the JSON Validator or check the YAML with the YAML Editor to confirm correctness before feeding it to your deployment pipeline.

Integrating with Kubernetes and Docker

Pkl is particularly valuable for Kubernetes and Docker configurations, where YAML manifests are verbose, repetitive, and error-prone at scale.

Kubernetes Deployment

k8s-deployment.pkl
class Container {
  name: String
  image: String
  port: Int
  resources: Resources = new {}
}

class Resources {
  cpuRequest: String = "100m"
  memoryRequest: String = "128Mi"
  cpuLimit: String = "500m"
  memoryLimit: String = "512Mi"
}

class Deployment {
  apiVersion: String = "apps/v1"
  kind: String = "Deployment"
  name: String
  namespace: String = "default"
  replicas: Int = 2
  container: Container
}

deployment: Deployment = new {
  name = "api-server"
  namespace = "production"
  replicas = 3
  container = new {
    name = "api"
    image = "registry.internal/api:v2.4.1"
    port = 8080
    resources = new {
      cpuRequest = "250m"
      memoryRequest = "256Mi"
      cpuLimit = "1000m"
      memoryLimit = "1Gi"
    }
  }
}

Evaluate this with pkl eval -f yaml k8s-deployment.pkl and pipe the output to kubectl apply -f -. You get YAML that kubectl understands, but you wrote it with type checking and validation.

Docker Compose

compose.pkl
class DockerService {
  image: String
  ports: Listing<String> = new {}
  environment: Mapping<String, String> = new {}
  volumes: Listing<String> = new {}
  depends_on: Listing<String> = new {}
}

version = "3.8"

services: Mapping<String, DockerService> = new {
  ["web"] = new {
    image = "nginx:alpine"
    ports = new { "80:80"; "443:443" }
    volumes = new { "./nginx.conf:/etc/nginx/nginx.conf:ro" }
    depends_on = new { "api" }
  }
  ["api"] = new {
    image = "node:20-slim"
    ports = new { "3000:3000" }
    environment = new {
      ["DATABASE_URL"] = "postgresql://db:5432/app"
      ["NODE_ENV"] = "production"
    }
    depends_on = new { "db" }
  }
  ["db"] = new {
    image = "postgres:16"
    ports = new { "5432:5432" }
    environment = new {
      ["POSTGRES_DB"] = "app"
      ["POSTGRES_USER"] = "admin"
      ["POSTGRES_PASSWORD"] = "changeme"
    }
    volumes = new { "pgdata:/var/lib/postgresql/data" }
  }
}
Terminal
pkl eval -f yaml -o docker-compose.yml compose.pkl
docker compose up -d

The advantage over writing docker-compose.yml directly: every service is validated against the DockerService class. If you forget a required field or assign a wrong type, Pkl tells you before Docker does.

IDE Support: VS Code, IntelliJ, Neovim

Pkl ships with first-party editor support for the three environments most developers use.

VS Code

Install the official extension from the VS Code marketplace by searching for apple.pkl-vscode or running:

Terminal
code --install-extension apple.pkl-vscode

The extension provides syntax highlighting, code completion, go-to-definition, inline error reporting, and formatting. It uses the Pkl Language Server under the hood.

IntelliJ IDEA

Install the Pkl plugin from the JetBrains Marketplace. It works with all JetBrains IDEs including WebStorm, PyCharm, and GoLand. The plugin provides the same language server features as the VS Code extension.

Neovim

Pkl provides a Tree-sitter grammar and LSP server. With nvim-lspconfig and nvim-treesitter, you get syntax highlighting, diagnostics, and completion. Add pkl to your Tree-sitter ensure_installed list and configure the LSP client to use the Pkl language server binary.

Language Server Protocol

Any editor that supports LSP can integrate with Pkl. The language server ships with the Pkl CLI distribution and provides diagnostics, completion, hover information, and go-to-definition. If your editor is not VS Code, IntelliJ, or Neovim, configure it to use the Pkl LSP binary as the language server for .pkl files.

Migration from YAML to Pkl

Migrating an existing YAML codebase to Pkl is a gradual process. You do not need to convert everything at once.

Step 1: Identify High-Value Targets

Start with YAML files that have the most duplication or the highest error rate. Kubernetes manifests, CI/CD pipelines with many similar jobs, and multi-environment configuration files are good candidates.

Step 2: Define Pkl Classes from Your YAML Structure

Look at your existing YAML and extract the common shape into Pkl classes.

Before: deployment.yaml (repeated per service)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
  namespace: production
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: user-service
          image: registry.io/user-service:v1.2.0
          ports:
            - containerPort: 8080
After: k8s-base.pkl
class K8sDeployment {
  name: String
  namespace: String = "default"
  replicas: Int(isPositive) = 2
  image: String
  containerPort: Int(isBetween(1, 65535)) = 8080
}

Step 3: Create Environment-Specific Amendments

user-service-prod.pkl
amends "k8s-base.pkl"

deployment = new K8sDeployment {
  name = "user-service"
  namespace = "production"
  replicas = 3
  image = "registry.io/user-service:v1.2.0"
}

Step 4: Update Your CI/CD Pipeline

Add a Pkl evaluation step before deployment. The generated YAML feeds into kubectl or any other tool that expects YAML input.

GitHub Actions snippet
- name: Install Pkl
  run: brew install pkl

- name: Generate Kubernetes manifests
  run: pkl eval -f yaml -m manifests/ configs/*.pkl

- name: Apply manifests
  run: kubectl apply -f manifests/

Step 5: Migrate Incrementally

Convert one service or one environment at a time. Pkl-generated YAML and hand-written YAML can coexist in the same deployment pipeline. There is no big-bang migration required.

During the migration, use the Code Diff Viewer to compare your original YAML against Pkl-generated output and confirm they produce identical results.

When to Use Pkl (and When Not To)

Pkl Is a Strong Choice When:

Stick with YAML, JSON, or TOML When:

The practical test: if you have ever spent time debugging a config error that a type system would have caught, or if you regularly copy-paste config blocks across files, Pkl is worth evaluating. If your configs are small and stable, the overhead is not worth it.

Related Developer Tools

Free browser-based tools for working with configuration files, validation, and format conversion.


Frequently Asked Questions

Pkl (pronounced "pickle") is an open-source configuration-as-code language created by Apple and released in February 2024 under the Apache 2.0 license. It provides a type-safe, programmable way to write configuration that can generate output in JSON, YAML, XML, and Java Properties formats. Unlike static formats like JSON or YAML, Pkl supports classes, functions, type constraints, and module inheritance, catching configuration errors at evaluation time rather than at deployment.

Pkl does not replace YAML and JSON directly because most tools still expect those formats as input. Instead, Pkl acts as a source-of-truth layer that generates YAML, JSON, XML, or property files. You write your configuration once in Pkl with type safety and validation, then run pkl eval to produce the output format your tools need. This means you get the benefits of type checking and reusable templates while your deployment pipeline continues to consume standard formats.

On macOS and Linux, the fastest way is Homebrew: run brew install pkl. On Windows, use the Windows Package Manager: winget install Apple.Pkl. You can also use mise (mise use -g pkl) on all three platforms, or download native binaries directly from the GitHub releases page at github.com/apple/pkl/releases. The native executables start instantly and run faster than the Java-based alternative. After installation, verify with pkl --version.

Yes. Pkl is well suited for Kubernetes and Docker configurations because it can generate YAML output that kubectl and docker-compose consume directly. You can define base templates for Kubernetes Deployments, Services, and ConfigMaps as Pkl classes, then amend them for each environment (development, staging, production) without duplicating entire manifest files. The type system catches errors like missing required fields or invalid port numbers before you apply anything to your cluster.

Pkl has official editor support for VS Code, IntelliJ IDEA, and Neovim. The VS Code extension (available on the marketplace as apple.pkl-vscode) provides syntax highlighting, code completion, go-to-definition, and inline error reporting. The IntelliJ plugin works with all JetBrains IDEs including WebStorm and PyCharm. Pkl also ships a Language Server Protocol (LSP) implementation, so any editor that supports LSP can provide basic Pkl support including diagnostics and completion.

NT

Christian Bucher

We build free developer tools including YAML formatters, JSON validators, config converters, and 269 more. All browser-based, no signup required.

269 Developer Tools, One Place

Browse 269 indexed tool pages with no QTool account required, and inspect the source on GitHub.

Open Source — Free Forever Try Free Tools

Related Articles

Built by Miguel

Need a custom tool or website?

From . Delivered in 24-48h. You own the code.

View Services →