Pkl 0.29 Release Notes

Pkl 0.29 was released on July 24th, 2025.
The latest bugfix release is 0.29.1. (All Versions)

This release brings support for working with binary data, and also a new setting to control HTTP rewriting.

The next release (0.30) is scheduled for October 2025. To see what’s coming in the future, follow the Pkl Roadmap.

Please send feedback and questions to GitHub Discussions, or submit an issue on GitHub.

Pkl is hosted on GitHub. To get started, follow Installation.

Highlights 💖

Bytes standard library class

A new standard library class is introduced, called Bytes (#1019).

Currently, Pkl does not have a built-in way to describe binary data. In situations where binary data is needed, the common pattern is to use a Base64 string. This is sufficient in simple cases, but still introduces many shortcomings:

  1. pkl eval can only produce UTF-8 encoded strings.

  2. It is not possible to compute the checksum of binary content (except in the case of read()).

  3. It is hard to interop with configuration formats that allow binary data.

  4. It is hard to do transformations on binary data.

To address these shortcomings, the Bytes class is introduced. This is a data type representing a sequence of bytes.

myBytes = Bytes(0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21) (1)
1 ASCII bytes for the string "Hello, World!"

To learn more about this feature, consult SPICE-0013.

Emitting binary output

A new property called bytes is added to FileOutput. The CLI has been changed to evaluate this property, and write its contents to the designated output location.

This change means that Pkl modules can now output binary content. For example, here is a simple module that outputs bytes:

output {
  bytes = Bytes(1, 2, 3, 4) (1)
}
1 Write bytes 1, 2, 3, 4

The same change applies when evaluating in multiple-file output mode.

output {
  files {
    ["foo.bin"] {
      bytes = Bytes(1, 2, 3, 4) (1)
    }
  }
}
1 Write bytes 1, 2, 3, 4 to a file called foo.bin.

Rendering Bytes

Out of the box, only the plist and pcf formats are able to render Bytes. For other formats, a renderer needs to be defined.

output {
  renderer = new JsonRenderer {
    [Bytes] = (it) -> it.base64 (1)
  }
}
1 Render bytes as a base64 string in JSON.

Using Bytes from language bindings

Users of Pkl’s language bindings also benefit from the new type. When using code generation, the Bytes data type will turn into the following types:

Language Type

Java

byte[]

Kotlin

ByteArray

Go

[]byte

Swift

[UInt8]

Maintainers of other language bindings are encouraged to map Bytes into the corresponding binary type in their language.

Bytes versus List<UInt8>

Conceptually, Bytes is very similar to List<UInt8>, because both are data types that describe sequences of UInt8 values. However, they have different performance characteristics. A List is a persistent data structure. This means that creating modified copies of lists is very cheap.

However, lists have more overhead per element. A List<UInt8> with 1000 elements takes up about 5.4 kilobytes of heap space, whereas the same data in Bytes takes roughly 1 kilobyte.

HTTP Rewrites and Package Mirroring

A new evaluator setting is introduced, which rewrites URLs before making outbound HTTP calls (#1062, #1127, #1133).

This setting can be configured via CLI flag --http-rewrite, and also in other ways:

  • A builder option in org.pkl.core.EvaluatorBuilder

  • A builder option in org.pkl.executor.ExecutorOptions

  • A new property in CreateEvaluatorRequest in the Message Passing API

  • A new property in the Gradle plugin

  • A new property in pkl.EvaluatorSettings#Http

The main use-case for this setting is to enable package mirroring. For example, let’s assume that the following mirrors exist:

Original Mirror

https://pkg.pkl-lang.org

https://my.internal.mirror/pkg-pkl-lang

https://github.com

https://my.internal.mirror/github

A user of the CLI can use these mirrors with the following settings.

~/.pkl/settings.pkl
amends "pkl:settings"

http {
  rewrites {
    ["https://pkg.pkl-lang.org/"] = "https://my.internal.mirror/pkg-pkl-lang/"
    ["https://github.com/"] = "https://my.internal.mirror/github/"
  }
}

To learn more about this feature, consult SPICE-0016.

Noteworthy 🎶

Ready when you need them.

pkldoc, pkl-codegen-java, pkl-codegen-kotlin executables

The pkldoc, pkl-config-java and pkl-config-kotlin CLIs are published as their own executables (#1023).

Currently, these tools have excellent support when called from within the pkl-gradle plugin. However, the story for using these as CLIs is much clunkier. Users must call Java by including their published .jar files, as well as all of their dependencies via the -classpath argument.

In 0.29, they are all being published as executables. In particular, pkldoc is being published as both a Java executable and a native executable. On the other hand, pkl-codegen-java and pkl-codegen-kotlin are published as Java executables only.

For more information, see their download instructions:

Java API changes

Resource Readers SPI added to preconfigured evaluators

A change was made to the preconfigured evaluators (#1094).

Currently, they add module key factories from service providers, but not resource readers.

This means that users of pkl-spring, for example, cannot add custom resource readers. This is also inconsistent (in the preconfigured evaluator, import can use custom schemes, but not read).

In Pkl 0.29, any resource reader registered via the Service Provider Interface will be added to the preconfigured evalutors.

New methods

New methods are introduced to the Java API.

  • org.pkl.core.Evaluator.evaluateOutputBytes

  • org.pkl.core.http.HttpClient.Builder.setRewrites

  • org.pkl.core.http.HttpClient.Builder.addRewrite

  • org.pkl.executor.ExecutorOptions.httpRewrites

  • org.pkl.config.java.ConfigEvaluatorBuilder.getHttpClient

  • org.pkl.config.java.ConfigEvaluatorBuilder.setHttpClient

pkl CLI changes

New features are added to the pkl CLI (#1052, #1056).

Aggregated JUnit reports

A new set of CLI flags are introduced: --junit-aggregate-reports, and --junit-aggregate-suite-name. Collectively, these flags tell Pkl to combine the JUnit reports into a single file, instead of creating a file per Pkl module.

Thanks to @gordonbondon for contributing to this feature!

shell-completion subcommand

A new subcommand called shell-completion has been added to the Pkl CLI.

This command produces autocompletion scripts for a given shell.

The following example installs shell completions for fish shell:

pkl shell-completion fish > "~/.config/fish/completions/pkl.fish"

Thanks to @gordonbondon for contributing to this feature!

@Generated annotation for Java/Kotlin codegen

Classes generated by the Java and Kotlin code generator can optionally recieve new annotation called Generated (#1075, #1115).

This behavior is toggled with the --generated-annotation CLI flag, or the similarly named Gradle property.

When enabled, classes are annotated with org.pkl.config.java.Generated.

This allows users of JaCoCo to exclude Pkl-generated classes from coverage reports.

Thanks to @arouel for their contributions here!

Breaking Changes 💔

Things to watch out for when upgrading.

Standard library deprecations

The following methods have been deprecated in the standard library:

Method name Details

Resource.md5

Replaced with Resource.bytes.md5

Resource.sha1

Replaced with Resource.bytes.sha1

Resource.sha256

Replaced with Resource.bytes.sha256

Resource.sha256Int

Replaced with Resource.bytes.sha256Int

Grammar changes

New rules have been introduced to the parser (#1022, #1126).

Block comment nesting is removed

Currently, block comments can be nested. For example, the comment /* /* my comment */ */ is two block comments; one outer comment and one inner comment.

However, this has some drawbacks.

  1. Block comments can be possibly be closed incorrectly, like /* /* my comment */. However, this is a valid block comment in most languages.

  2. Pkl’s syntax highlighters do not support block comment nesting, leading to highlighting that is inconsistent with Pkl’s parser.

To improve user experience, block comment nesting is removed. As a result, block comments are always terminated upon the first */.

Shebang line can only appear at the start of a file

The grammar around shebang comments has been made stricter.

Pkl files allow for a shebang comment. Currently, this comment can appear anywhere in a file. In 0.29, this comment must appear at the start of a file.

Opaque file: URIs are invalid

A new rule is introduced to treat opaque file: URIs as errors (#1087).

Opaque file URIs are URIs whose scheme-specific part does not start with /. For example, file:foo/bar.txt is an opaque URI.

Currently, this has the unintentional behavior of: look for file foo/bar.txt from the process working directory. These are effectively dynamics imports; from a single import, we can’t statically determine what file is actually being imported.

According to RFC-8089, file URIs must have paths that start with /. So, these are actually not valid URIs.

In Pkl 0.29, it is an error to load a module or resource with an opaque file: URI.

To import or read a relative path, omit file: from the import string. For example, import("foo/bar.txt") instead of import("file:foo/bar.txt").

New base module names: Bytes and Charset

Two new names are introduced to the base module: Bytes and Charset. That means that any code that currently references these names on implicit this will break (#1019).

The following snippet demonstrates this breaking behavior. In 0.28 and below, obj2.prop resolves to string "my bytes". In 0.29, it resolves to class Bytes in the base module.

obj1 {
  Bytes = "my bytes"
}

obj2 = (obj1) {
  prop = Bytes
}

To make this code continue to have the same meaning, an explicit this reference is required.

 obj1 {
   Bytes = "my bytes"
 }

 obj2 = (obj1) {
-  prop = Bytes
+  prop = this.Bytes
 }

This only affects code that references these names off of implicit this. Code that references the name from the lexical scope will continue to work as-is.

To learn more about name resolution, consult the language reference.

jpkl is not published to Maven Central

Due to a breakage in release pipeline, the jpkl executable is not published to Maven Central (#1147).

It is still available to download as a GitHub release asset.

Miscellaneous 🐸

  • Documentation improvements (#1065, #1127).

  • Dependency updates (#1088, #1128, #1139).

  • Use javac -release and kotlinc -Xjdk-release compiler flags for improved bytecode compatibilty (#1080).

  • Parser improvements (#1066).

Bugs fixed 🐜

The following bugs have been fixed.

  • New parser fails on nested multi line comments (#1014)

  • Fix package dependency links when generating pkldoc (#1078)

  • Don’t show 100% when number of failures is rounded up (#1110)

  • Quoting the module name crashes pkl (#1111)

  • Shebang comment parsing is too lenient (#1125)

  • CLI: noProxy option in settings.pkl and PklProject are ignored (#1142)

Contributors 🙏

We would like to thank the contributors to this release (in alphabetical order):