Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/modules/release-notes/pages/0.33.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ Native `pkl` and `pkldoc` binaries for Intel Mac systems are no longer published

To continue running new Pkl releases on these systems, use an appropriate Java runtime and the `jpkl` and `jpkldoc` Java executables.

=== JSON and YAML Parsers Reject the Object Key `default`

`Dynamic` declares a hidden `default` property, so an object key named `default` could not be represented as a `Dynamic` property.
Previously, `json.Parser.parse()`, `yaml.Parser.parse()` and `yaml.Parser.parseAll()` silently produced an object that was missing the key, and reading `.default` on it failed with a confusing type error.

These methods now throw, and the error message points at `useMapping = true`, which parses into a `Mapping` and preserves the key:

[source,pkl]
----
new json.Parser { useMapping = true }.parse("""
{"default": "greeting"}
""")
----

=== XXX

== Bug Fixes [small]#🐜#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.pkl.core.stdlib.ExternalMethod1Node;
import org.pkl.core.stdlib.PklConverter;
import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.LateInit;
import org.pkl.core.util.json.JsonHandler;
import org.pkl.core.util.json.JsonParser;
Expand Down Expand Up @@ -168,7 +169,13 @@ public void endObject(@Nullable EconomicMap<Object, ObjectMember> members) {

@Override
public void startObjectValue(@Nullable EconomicMap<Object, ObjectMember> members, String name) {
currPath.push(Identifier.get(name));
var identifier = Identifier.get(name);
// https://github.com/apple/pkl/issues/561
if (!useMapping && identifier == Identifier.DEFAULT) {
throw new ParseException(
ErrorMessages.create("jsonParseErrorDynamicPropertyDefault"), getLocation());
}
currPath.push(identifier);
}

@Override
Expand Down
15 changes: 15 additions & 0 deletions pkl-core/src/main/java/org/pkl/core/stdlib/yaml/ParserNodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import org.pkl.core.stdlib.ExternalMethod1Node;
import org.pkl.core.stdlib.PklConverter;
import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.yaml.ParseException;
import org.pkl.core.util.yaml.snake.YamlUtils;
import org.snakeyaml.engine.v2.api.ConstructNode;
import org.snakeyaml.engine.v2.api.Load;
Expand Down Expand Up @@ -72,6 +74,9 @@ private Object doParse(VmTyped self, String text, String uri) {
var document = load.loadFromString(text);
return converter.convert(document, List.of());
} catch (YamlEngineException e) {
if (e.getCause() instanceof ParseException cause) {
throw exceptionBuilder().evalError("yamlParseError").withHint(cause.getMessage()).build();
}
if (e.getMessage()
.startsWith("Number of aliases for non-scalar nodes exceeds the specified")) {
throw exceptionBuilder()
Expand Down Expand Up @@ -110,6 +115,9 @@ private VmList doParseAll(VmTyped self, String text, String uri) {
builder.add(converter.convert(document, List.of(VmValueConverter.TOP_LEVEL_VALUE)));
}
} catch (YamlEngineException e) {
if (e.getCause() instanceof ParseException cause) {
throw exceptionBuilder().evalError("yamlParseError").withHint(cause.getMessage()).build();
}
if (e.getMessage()
.startsWith("Number of aliases for non-scalar nodes exceeds the specified")) {
throw exceptionBuilder()
Expand Down Expand Up @@ -480,6 +488,13 @@ private void addMembers(MappingNode node, VmObject object) {
var memberName =
convertedKey instanceof String string && !useMapping ? Identifier.get(string) : null;

// https://github.com/apple/pkl/issues/561
if (memberName == Identifier.DEFAULT) {
throw new ParseException(
ErrorMessages.create("yamlParseErrorDynamicPropertyDefault"),
keyNode.getStartMark().orElse(null));
}

var member =
new ObjectMember(
sourceSection,
Expand Down
48 changes: 48 additions & 0 deletions pkl-core/src/main/java/org/pkl/core/util/yaml/ParseException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.util.yaml;

import org.jspecify.annotations.Nullable;
import org.snakeyaml.engine.v2.exceptions.Mark;

/**
* An unchecked exception to indicate that a YAML document cannot be represented as the requested
* Pkl value.
*
* <p>Unlike {@code YamlEngineException}, whose messages originate from SnakeYAML, messages carried
* by this exception are written by Pkl and are suitable for presenting to users as-is.
*
* <p>SnakeYAML wraps any exception other than {@code YamlEngineException} thrown during
* construction, so this exception is observed as the cause of a {@code YamlEngineException} rather
* than caught directly.
*/
public final class ParseException extends RuntimeException {
private final @Nullable Mark location;

public ParseException(String message, @Nullable Mark location) {
super(location == null ? message : message + location);
this.location = location;
}

/**
* Returns the location at which the error occurred.
*
* @return the error location
*/
public @Nullable Mark getLocation() {
return location;
}
}
10 changes: 10 additions & 0 deletions pkl-core/src/main/resources/org/pkl/core/errorMessages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,11 @@ Converter path `{0}` has invalid syntax.
jsonParseError=\
Error parsing JSON document.

jsonParseErrorDynamicPropertyDefault=\
Cannot parse an object with key `"default"` into a `Dynamic`.\
\n\
Try parsing into a `Mapping` instead, by setting `useMapping = true` in the `pkl.json#Parser`.

yamlParseError=\
Error parsing YAML document.

Expand All @@ -806,6 +811,11 @@ Error parsing YAML document: The number of aliases for collection nodes exceeds
\n\
To increase the allowed maximum, set `YamlRenderer.maxCollectionAliases`.

yamlParseErrorDynamicPropertyDefault=\
Cannot parse an object with key `default` into a `Dynamic`.\
\n\
Try parsing into a `Mapping` instead, by setting `useMapping = true` in the `pkl.yaml#Parser`.

evaluationTimedOut=\
Evaluation timed out after {0,number,#.##} second(s).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,19 @@ res16 = parser.parse("""

// invalid syntax
res17 = test.catch(() -> parser.parse("!@#$%"))

// object key `default` cannot be represented as a `Dynamic` property
// https://github.com/apple/pkl/issues/561
res18 = test.catch(() -> parser.parse("""
{"default": null}
"""))

// nested objects are rejected too
res19 = test.catch(() -> parser.parse("""
{"person": {"default": null}}
"""))

// parsing into a `Mapping` keeps working
res20 = (parser) { useMapping = true }.parse("""
{"default": null}
""")
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,25 @@ res15 = parser.parseAll("""
hobby: surfing
...
""")

// object key `default` cannot be represented as a `Dynamic` property
// https://github.com/apple/pkl/issues/561
res16 = test.catch(() -> parser.parse("""
default: null
"""))

// nested mappings are rejected too
res17 = test.catch(() -> parser.parse("""
person:
default: null
"""))

// `parseAll` rejects it as well
res18 = test.catch(() -> parser.parseAll("""
default: null
"""))

// parsing into a `Mapping` keeps working
res19 = (parser) { useMapping = true }.parse("""
default: null
""")
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import "pkl:json"

// https://github.com/apple/pkl/issues/561
res = new json.Parser {}.parse("""
{
"hello": "world",
"default": "greeting"
}
""")
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import "pkl:yaml"

// https://github.com/apple/pkl/issues/561
res = new yaml.Parser {}.parse("""
hello: world
default: greeting
""")
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,8 @@ res16 {
}
}
res17 = "Error parsing JSON document."
res18 = "Error parsing JSON document."
res19 = "Error parsing JSON document."
res20 {
["default"] = null
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,9 @@ res15 = List(new {
}, new {
hobby = "surfing"
})
res16 = "Error parsing YAML document."
res17 = "Error parsing YAML document."
res18 = "Error parsing YAML document."
res19 {
["default"] = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
–– Pkl Error ––
Error parsing JSON document.

x | res = new json.Parser {}.parse("""
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at jsonParserDefaultProperty#res (file:///$snippetsDir/input/errors/jsonParserDefaultProperty.pkl)

Cannot parse an object with key `"default"` into a `Dynamic`.
Try parsing into a `Mapping` instead, by setting `useMapping = true` in the `pkl.json#Parser`. at 3:14

xxx | renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)

xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8")
^^^^
at pkl.base#Module.output.bytes (pkl:base)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
–– Pkl Error ––
Error parsing YAML document.

x | res = new yaml.Parser {}.parse("""
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at yamlParserDefaultProperty#res (file:///$snippetsDir/input/errors/yamlParserDefaultProperty.pkl)

Cannot parse an object with key `default` into a `Dynamic`.
Try parsing into a `Mapping` instead, by setting `useMapping = true` in the `pkl.yaml#Parser`. in input_string, line 2, column 1:
default: greeting
^

xxx | renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)

xxx | if (renderer is BytesRenderer) renderer.renderDocument(value) else text.encodeToBytes("UTF-8")
^^^^
at pkl.base#Module.output.bytes (pkl:base)