by the Pkl Team on February 1st, 2024

We are delighted to announce the open source first release of Pkl (pronounced Pickle), a programming language for producing configuration.

When thinking about configuration, it is common to think of static languages like JSON, YAML, or Property Lists.
While these languages have their own merits, they tend to fall short when configuration grows in complexity.
For example, their lack of expressivity means that code often gets repeated.
Additionally, it can be easy to make configuration errors, because these formats do not provide any validation of their own.

To address these shortcomings, sometimes formats get enhanced by ancillary tools that add special logic.
For example, perhaps there’s a need to make code more DRY, so a special property is introduced that understands how to resolve references, and merge objects together.
Alternatively, there’s a need to guard against validation errors, so some new way is created to validate a configuration value against an expected type.
Before long, these formats almost become programming languages, but ones that are hard to understand and hard to write.

On the other end of the spectrum, a general-purpose language might be used instead.
Languages like Kotlin, Ruby, or JavaScript become the basis for DSLs that generate configuration data.
While these languages are tremendously powerful, they can be awkward to use for describing configuration, because they are not oriented around defining and validating data.
Additionally, these DSLs tend to be tied to their own ecosystems.
It is a hard sell to use a Kotlin DSL as the configuration layer for an application written in Go.

We created Pkl because we think that configuration is best expressed as a blend between a static language and a general-purpose programming language.
We want to take the best of both worlds; to provide a language that is declarative and simple to read and write, but enhanced with capabilities borrowed from general-purpose languages.
When writing Pkl, you are able to use the language features you’d expect, like classes, functions, conditionals, and loops.
You can build abstraction layers, and share code by creating packages and publishing them.
Most importantly, you can use Pkl to meet many different types of configuration needs.
It can be used to produce static configuration files in any format, or be embedded as a library into another application runtime.

We designed Pkl with three overarching goals:

To provide safety by catching validation errors before deployment.

To scale from simple to complex use-cases.

To be a joy to write, with our best-in-class IDE integrations.

A Quick Tour of Pkl

We created Pkl to have a familiar syntax to developers, and to be easy to learn. That is why we’ve included features like classes, functions, loops, and type annotations.

For example, here is a Pkl file (module) that defines a configuration schema for an imaginary web application.

This file defines types, and not data. This is a common pattern in Pkl, and we call this a template.

Application.pkl

module Application

hostname: String

port: UInt16

environment: Environment

database: Database

class Database {

username: String

password: String

host: String

port: UInt16

dbName: String
}

typealias Environment = “dev”|”qa”|”prod”

And here is how configuration data might be defined:

localhost.pkl

amends “Application.pkl”

hostname = “localhost”

port = 3599

environment = “dev”

database {
host = “localhost”
port = 5786
username = “admin”
password = read(“env:DATABASE_PASSWORD”) (1)
dbName = “myapp”
}

It is easy to create variations of the same base data by amending.
For example, let’s imagine that we want to run four databases locally, as sidecars.
This uses a for generator to produce four variations, each of which amends the base db and specifies a different port.

sidecars.pkl

import “Application.pkl”

hidden db: Application.Database = new {
host = “localhost”
username = “admin”
password = read(“env:DATABASE_PASSWORD”)
dbName = “myapp”
}

sidecars {
for (offset in List(0, 1, 2, 3)) {
(db) {
port = 6000 + offset
}
}
}

Pkl programs can be easily rendered to common formats.

$ export DATABASE_PASSWORD=hunter2
$ pkl eval –format yaml sidecars.pkl

sidecars:
– username: admin
password: hunter2
host: localhost
port: 6000
dbName: myapp
– username: admin
password: hunter2
host: localhost
port: 6001
dbName: myapp
– username: admin
password: hunter2
host: localhost
port: 6002
dbName: myapp
– username: admin
password: hunter2
host: localhost
port: 6003
dbName: myapp

$ export DATABASE_PASSWORD=hunter2
$ pkl eval –format json sidecars.pkl

{
“sidecars”: [
{
“username”: “admin”,
“password”: “hunter2”,
“host”: “localhost”,
“port”: 6000,
“dbName”: “myapp”
},
{
“username”: “admin”,
“password”: “hunter2”,
“host”: “localhost”,
“port”: 6001,
“dbName”: “myapp”
},
{
“username”: “admin”,
“password”: “hunter2”,
“host”: “localhost”,
“port”: 6002,
“dbName”: “myapp”
},
{
“username”: “admin”,
“password”: “hunter2”,
“host”: “localhost”,
“port”: 6003,
“dbName”: “myapp”
}
]
}

$ export DATABASE_PASSWORD=hunter2
$ pkl eval –format xml sidecars.pkl

admin
hunter2
localhost
6000
myapp

admin
hunter2
localhost
6001
myapp

admin
hunter2
localhost
6002
myapp

admin
hunter2
localhost
6003
myapp

Built-in Validation
Configuration is about data. And data needs to be valid.

In Pkl, validation is achieved using type annotations.
And, type annotations can optionally have constraints defined on them.

Here is an example, that defines the following constraints:

age must be between 0 and 130.

name to not be empty.

zipCode must be a string with five digits.

Person.pkl

module Person

name: String(!isEmpty)

age: Int(isBetween(0, 130))

zipCode: String(matches(Regex(“\d{5}”)))

A failing constraint causes an evaluation error.

alessandra.pkl

amends “Person.pkl”

name = “Alessandra”

age = -5

zipCode = “90210”

Evaluating this module fails:

$ pkl eval alessandra.pkl
–– Pkl Error ––
Type constraint `isBetween(0, 130)` violated.
Value: -5

5 | age: Int(isBetween(0, 130))
^^^^^^^^^^^^^^^^^
at Person#age (file:///Person.pkl)

5 | age = -5
^^
at alessandra#age (file:///alessandra.pkl)

106 | text = renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (https://github.com/apple/pkl/blob/0.25.0/stdlib/base.pkl#L106)

Constraints are arbitrary expressions.
This allows you to author types that can express any type of check that can be expressed in Pkl.
Here is a sample type that must be a string with an odd length, and whose first letter matches the last letter.

name: String(length.isOdd, chars.first == chars.last)

Sharing Packages
Pkl provides the ability to publish packages, and to import them as dependencies in a project.
This provides an easy way to share Pkl code that can be used in other projects.

It is easy to create your own package and publish them as GitHub releases, or to upload them anywhere you wish.

Packages can be imported via the absolute URI:

import “package://pkg.pkl-lang.org/pkl-pantry/pkl.toml@1.0.0#/toml.pkl”

output {
renderer = new toml.Renderer {}
}

Alternatively, they can be managed as dependencies of a project.
Using a project allows Pkl to resolve version conflicts between different versions of the same dependency within a dependency graph.
It also means that you can import packages by a simpler name.

PklProject

amends “pkl:Project”

dependencies {
[“toml”] { uri = “package://pkg.pkl-lang.org/pkl-pantry/pkl.toml@1.0.0” }
}

myconfig.pkl

import “@toml/toml.pkl”

output {
renderer = new toml.Renderer {}
}

A set of packages are maintained by us, the Pkl team. These include:

pkl-pantry — a monorepo that publishes many different packages.

pkl-k8s — templates for defining Kubernetes descriptors.

Language Bindings

Pkl can produce configuration as textual output, and it can also be embedded as a library into other languages via our language bindings.

When binding to a language, Pkl schema can be generated as classes/structs in the target language.
For example, the Application.pkl example from above can be generated into Swift, Go, Java, and Kotlin.
Pkl even includes documentation comments in the target language.

import PklSwift

public enum Application {}

extension Application {
public enum Environment: String, CaseIterable, Decodable, Hashable {
case dev = “dev”
case qa = “qa”
case prod = “prod”
}

public struct Module: PklRegisteredType, Decodable, Hashable {
public static var registeredIdentifier: String = “Application”

/// The hostname that this server responds to.
public var hostname: String

/// The port to listen on.
public var port: UInt16

/// The environment to deploy to.
public var environment: Environment

/// The database connection for this application
public var database: Database

public init(hostname: String, port: UInt16, environment: Environment, database: Database) {
self.hostname = hostname
self.port = port
self.environment = environment
self.database = database
}
}

public struct Database: PklRegisteredType, Decodable, Hashable {
public static var registeredIdentifier: String = “Application#Database”

/// The username for this database.
public var username: String

/// The password for this database.
public var password: String

/// The remote host for this database.
public var host: String

/// The remote port for this database.
public var port: UInt16

/// The name of the database.
public var dbName: String

public init(username: String, password: String, host: String, port: UInt16, dbName: String) {
self.username = username
self.password = password
self.host = host
self.port = port
self.dbName = dbName
}
}
}

Application.pkl.go

package application

type Application struct {
// The hostname that this server responds to.
Hostname string `pkl:”hostname”`

// The port to listen on.
Port uint16 `pkl:”port”`

// The environment to deploy to.
Environment Environment.Environment `pkl:”environment”`

// The database connection for this application
Database *Database `pkl:”database”`
}

Database.pkl.go

// Code generated from Pkl module `Application`. DO NOT EDIT.
package application

type Databas
Read More