apple/pkl · error · ParseException
valid string character
Error message
valid string character
What it means
Inside a JSON string, all characters must be either an escape sequence (backslash-prefixed) or a character >= 0x20. readStringInternal throws expected("valid string character") when it encounters an unescaped control character (e.g. raw tab, newline, or any byte < 0x20) inside a string literal. Strict JSON forbids literal control characters in strings.
Solutions
- Escape control characters: use \n, \t, \r, \uXXXX instead of raw bytes inside the string.
- Serialize the text with a proper JSON encoder (JSON.stringify / Jackson / json.dumps) instead of hand-concatenating strings into JSON.
- Strip or replace invalid control characters (chars < 0x20) from the input before parsing if the data is known to be dirty.
- Use the ParseException's line/column to find the offending character in the document.
Example fix
// before
String json = "{\"text\": \"line1
line2\"}"; // raw newline in string
// after
String json = "{\"text\": \"line1\\nline2\"}"; Defensive patterns
Strategy: validation
Validate before calling
// Java: reject raw control characters inside strings before parsing
static boolean hasRawControlChars(String json) {
boolean inStr = false, esc = false;
for (char c : json.toCharArray()) {
if (esc) { esc = false; continue; }
if (c == '\\' && inStr) { esc = true; continue; }
if (c == '"') inStr = !inStr;
else if (inStr && c < 0x20) return true;
}
return false;
} Try / catch
try {
parser.parse(json);
} catch (ParseException e) {
if (e.getMessage().contains("valid string character")) {
throw new IllegalArgumentException("Unescaped control character in JSON string at " + e.getLocation().line + ":" + e.getLocation().column, e);
}
throw e;
} Prevention
- Escape \n, \t, \r in strings; never embed raw newlines.
- Build JSON with a serializer that escapes automatically.
- Sanitize text from logs/binary sources before embedding it in JSON.
- Detect and strip stray control bytes from externally produced files.
When it happens
Trigger: parse() on strings containing raw newlines/tabs (multi-line strings pasted into JSON), raw escape bytes like 0x0B, or binary/corrupt data inside a quoted string.
Common situations: Copy-pasting multi-line text into a JSON string without escaping newlines, log files or binary blobs embedded in JSON, Windows-generated files with stray control characters, heredocs embedded in config.
Understand the failure class
Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.
Related errors
AI-assisted analysis of apple/pkl@f3efcbfc9b (2026-09-08).
Data as JSON: /api/errors/513b78d0a2eb190b.
Report an issue: GitHub.
Appendix: source
Thrown at pkl-core/src/main/java/org/pkl/core/util/json/JsonParser.java:258
throw expected("'" + ch + "'");
}
}
private void readString() throws IOException {
handler.startString();
handler.endString(readStringInternal());
}
private String readStringInternal() throws IOException {
read();
startCapture();
while (current != '"') {
if (current == '\\') {
pauseCapture();
readEscape();
startCapture();
} else if (current < 0x20) {
throw expected("valid string character");
} else {
read();
}
}
var string = endCapture();
read();
return string;
}
private void readEscape() throws IOException {
read();
switch (current) {
case '"', '/', '\\' -> captureBuffer.append((char) current);
case 'b' -> captureBuffer.append('\b');
case 'f' -> captureBuffer.append('\f');
case 'n' -> captureBuffer.append('\n');
case 'r' -> captureBuffer.append('\r');
case 't' -> captureBuffer.append('\t');View on GitHub (pinned to f3efcbfc9b)