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:
- Type-safe. Every value has a type. Pkl catches type mismatches at evaluation time, before your configuration reaches any deployment pipeline.
- Programmable. Classes, functions, conditionals, loops, and string interpolation. You can abstract repeated patterns into reusable modules.
- Multi-format output. A single Pkl source file can generate JSON, YAML, XML, or Java Properties. Your tools do not need to understand Pkl directly.
- Validatable. Type annotations support constraints like
port: Int(isBetween(1, 65535))that catch invalid values during evaluation. - Package ecosystem. Pkl modules can be published and imported as versioned packages, similar to npm or Go modules.
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)
brew install pkl
pkl --version
Windows (winget)
winget install Apple.Pkl
pkl --version
Cross-Platform (mise)
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.
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
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.
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.
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.
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.
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.
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.
amends "base-service.pkl"
service {
name = "user-service"
replicas = 1
env {
["LOG_LEVEL"] = "debug"
["NODE_ENV"] = "staging"
}
}
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.
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
pkl eval -f yaml production.pkl
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
pkl eval -f json production.pkl
{
"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
pkl eval -f properties production.pkl
Write Output to Files
# 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
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
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" }
}
}
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:
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.
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
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
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.
- 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:
- Configuration is duplicated across environments. If you maintain dev, staging, and production variants of the same config, Pkl's template and amend system eliminates the duplication.
- Configuration errors reach production. Type constraints catch invalid values during evaluation, before deployment.
- Multiple output formats are needed. A single Pkl source can generate YAML for Kubernetes, JSON for application config, and properties files for Java services.
- Teams share configuration libraries. Pkl's package system lets you publish and version reusable config modules across projects.
- Config complexity is growing. When YAML files exceed a few hundred lines and YAML anchors are not enough to manage repetition, Pkl's classes and modules provide real abstraction.
Stick with YAML, JSON, or TOML When:
- Configuration is simple and static. A 20-line YAML file does not need a type system.
- Your team is small and the config rarely changes. The learning curve of Pkl is not justified if config errors are rare.
- Tooling does not support a build step. Some platforms expect to read config files directly. Adding a Pkl evaluation step may not be possible or practical.
- Every team member needs to edit config. Pkl requires learning new syntax. If non-developers need to modify configuration, YAML or TOML are more accessible.
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.