quote identifier keys above U+FFFF when ensure_ascii is true by spicysharkco · Pull Request #128 · dpranke/pyjson5 · GitHub
Skip to content

quote identifier keys above U+FFFF when ensure_ascii is true - #128

Merged
dpranke merged 2 commits into
dpranke:mainfrom
spicysharkco:fix/astral-unquoted-keys
Oct 22, 2026
Merged

dpranke merged 2 commits into
dpranke:mainfrom
spicysharkco:fix/astral-unquoted-keys

Conversation

@spicysharkco

Copy link
Copy Markdown
Contributor

With the default ensure_ascii=True, json5.dumps({'\U0001d49c': 1}) returns {\ud835\udc9c: 1}. The unquoted-key branch from #124 escapes every non-ascii character of an identifier key, and a character above U+FFFF becomes a surrogate pair of two \u escapes. Each half is a lone surrogate, which is not an identifier character, so the text is not valid JSON5. The json5 npm package (2.2.3) rejects it with "invalid identifier character at 1:2", and json-five (1.1.2) rejects it too. pyjson5's own reader accepts it, so loads(dumps(x)) does not show the problem.

Write such a key as a quoted string when ensure_ascii is true, the way quote_keys=True does. Keys inside the BMP stay unquoted, and with ensure_ascii=False the character is still written unquoted. The unquoted-key branch:

pyjson5/json5/lib.py

Lines 709 to 719 in 2dfeb36

test_ensure_ascii_astral_keys covers a key that starts with the character, one with it between ASCII letters, and U+10000, plus the ensure_ascii=False output.

With ensure_ascii true (the default), dumps() writes a non-ASCII
identifier key unquoted, with \u escapes. A character above U+FFFF
becomes a surrogate pair of two escapes, so dumps({'\U0001d49c': 1})
returned {\ud835\udc9c: 1}.

Each half of the pair is a lone surrogate, which is not an identifier
character. The json5 npm package (2.2.3) rejects that text with
"invalid identifier character", and json-five (1.1.2) rejects it too.
pyjson5's own reader accepts it, so loads() did not show the problem.

Write such a key as a quoted string when ensure_ascii is true, as
quote_keys=True already does. Keys that are all in the BMP are
unchanged, and with ensure_ascii false the character is still written
unquoted.

The new test checks a key that starts with the astral character, one
with it between ASCII letters, and the first code point above the BMP,
plus the ensure_ascii=False output and a round trip through loads().
@dpranke

dpranke commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Thanks for the patch! This sounds plausible, but I'll try to take a closer look at this tomorrow (sorry for the delay).

@dpranke

dpranke commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Okay, I've looked at this and I think it's probably fine, but I have a question first:

IIUC, in current versions of JavaScript, you can specify a supplemental (not-BMP) character with \u{1d49c}, but not ES5.1, and the JSON5 spec is based on ES5.1. So, {\u{1d49c}: 4} is legal JS but not JSON5.

And {\ud835\udc9c: 4} is neither legal JSON5 nor legal JavaScript (either in 5.1 or now).

Is that all correct (as far as you know)? If so, I think this patch is fine, but the comment is slightly misleading. Instead of saying "Other readers reject a surrogate pair", wouldn't it be better to say that they are not allowed?

@spicysharkco

Copy link
Copy Markdown
Contributor Author

Yes, that matches what I find:

  • \u{1d49c} is valid in current JavaScript (ECMA-262 §12.7.1), but ES5.1 escapes take exactly four hex digits (ES5.1 §7.6, §7.8.4), and JSON5 keys are ES5.1 IdentifierNames (JSON5 §1.1).
  • {𝒜: 4} is not allowed in either: each escape must itself be an identifier character, and the two are not combined (ES5.1 §7.6, ECMA-262 §12.7.1.1). Node and the json5 package both reject it.

I changed the comment to say it is not allowed (976c14e).

@dpranke

dpranke commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Wait, I'm not sure what you mean by "{𝒜: 4} is not allowed in either"?

I can load that just fine in current Node (v26) (as a straight expression) and current JSON5.parse. That should work, shouldn't it? 𝒜 is a letter (unicode category Lu). It works in both current JavaScript (node v26) and that + the current JSON5 package.

@dpranke

dpranke commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Also, I'm curious: how did you even find this bug, given that I haven't released a version that has it yet? Are you actually working off of HEAD in the repo (and, if so, why)? It's fine if you are, I just don't typically expect people to be doing so.

@spicysharkco

Copy link
Copy Markdown
Contributor Author

Sorry, that was my mistake: the backslashes fell out of my reply. That line was about the escaped form from your comment, {\ud835\udc9c: 4}, which Node and json5 both reject. You're right that {𝒜: 4} is fine: Node 26.10.0 and json5 2.2.3 both parse it. With ensure_ascii=False the patch still writes that key unquoted, and the test checks that.

Yes, I was working from HEAD. I review main rather than the last release, so a bug can be fixed before it ships. I found this one by parsing dumps output with the json5 npm package: pyjson5 reads its own output back, so a round trip alone does not show it.

@dpranke

dpranke commented Oct 22, 2026

Copy link
Copy Markdown
Owner

@dpranke
dpranke merged commit 923dc45 into dpranke:main Oct 22, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants