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

  1. Escape control characters: use \n, \t, \r, \uXXXX instead of raw bytes inside the string.
  2. Serialize the text with a proper JSON encoder (JSON.stringify / Jackson / json.dumps) instead of hand-concatenating strings into JSON.
  3. Strip or replace invalid control characters (chars < 0x20) from the input before parsing if the data is known to be dirty.
  4. 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

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)