Evaluator API

pkl-swift provides a rich API for evaluating Pkl files in Swift via PklSwift.Evaluator. It can be configured with custom readers, have its own security settings, control external property inputs, and more.

Evaluation occurs by spawning the pkl cli as a child process, and communicates via message passing.

Using evaluators

There are two methods for interacting with PklSwift.Evaluator:

  1. PklSwift.withEvaluator(options:_:)

  2. PklSwift.EvaluatorManager.withEvaluator(options:_:)

These two methods are distinguished by how they manage the lifecycle of the underlying pkl child process. PklSwift.withEvaluator(options:_:) will spawn a child process per-evaluator, whereas PklSwift.EvaluatorManager.withEvaluator(options:_:) will spawn a child process for the lifetime of the evaluator manager.

For most use cases, it is sufficient to use PklSwift.withEvaluator(options:_:). If multiple evaluators are desired, for example, to maintain separate caches or to have different settings, the evaluator manager should be used.

Evaluating modules

With code generation

Swift structs generated from Pkl modules may be evaluated simply through the generated loadFrom(source:) and loadFrom(evaluator:source:) methods.

For example, a Pkl module can be evaluated into a generated struct like so:

@main struct Main {
    static func main() async throws {
        let config = try await AppConfig.loadFrom(source: .path("myconfig.pkl"))
        print("Got config: \(config)")

Without code generation

Pkl modules may be evaluated directly into Swift structs without using code generation.

For example, given the Pkl module:

foo: String = "hello"

bar: Int = 5

A Swift struct can be created matching the shape of the Pkl module. The Swift module must be Decodable.

struct Foo: Decodable {
    let foo: String
    let bar: Int

Then, provide Foo.self to the evaluateModule method:

let result = try await withEvaluator { evaluator in
    try await evaluator.evaluateModule(source: .path("foo.pkl"), as: Foo.self)

If the Swift property name doesn’t match the Pkl name, create a nested CodingKeys enum that describes how Swift names turn into Pkl names.

For more details, consult the Swift documentation.

struct Foo: Decodable {
    let myFoo: String
    let myBar: Int

    enum CodingKeys: String, CodingKey {
        case myFoo = "foo"
        case myBar = "bar"

Alternative evaluation modes

Textual and multiple file output

In addition to modules, an evaluator may also evaluate a module’s output.text and output.files properties. This mimics the behavior of the CLI.

Take this Pkl module:

foo = "foo"

bar = "bar"

The textual output of this module may be evaluated via PklSwift.Evaluator.evaluateOutputText(source:).

@main struct Main {
    static func main() async throws {
        let textOutput = try await withEvaluator { evaluator in
            try await evaluator.evaluateOutputText(source: .path("my/module.pkl"))


In addition to evaluating modules and textual/file output, any arbitrary expression may be evaluated within a module. In fact, the textual and file output of a module are implemented in terms of evaluating an expression.

@main struct Main {
    static func main() async throws {
        let res = try await withEvaluator { evaluator in
            try await evaluator.evaluateExpression(
                source: .text("foo = 5"), (1)
                expression: "foo + 10", (2)
                as: Int.self (3)
        print("foo is \(res)") // prints "foo is 15"
1 .text("foo = 5") causes Pkl to evaluate a synthetic module whose contents are foo = 5.
2 The expression to be evaluated.
3 The expression’s result type.

Evaluator options

An evaluator is configured via options on PklSwift.EvaluatorOptions. A sensible default set of options is provided on PklSwift.EvaluatorOptions.preconfigured. Alternatively, they can be built from empty by starting with PklSwift.EvaluatorOptions.empty.

Custom readers

It is possible to use a custom reader for resources and modules by implementing the PklSwift.ResourceReader and PklSwift.ModuleReader protocols.

Custom readers must identify the scheme that they are responsible for reading by setting the scheme field. For example, a reader may be registered to resolve the Pkl expression read("secret:FOO") by registering "secret" as its scheme.

If a resource matches a scheme identified by a custom reader, its read() method will be called to retrieve the contents.