Skip to content

fix: coerce string quote_style values into QuoteStyle members - #141

Merged
dpranke merged 3 commits into
dpranke:mainfrom
rahul05ranjan:fix-quote-style-string-coercion
Nov 1, 2026
Merged

dpranke merged 3 commits into
dpranke:mainfrom
rahul05ranjan:fix-quote-style-string-coercion

Conversation

@rahul05ranjan

Copy link
Copy Markdown
Contributor

Fixes #140.

Problem

dumps() documents quote_style as a QuoteStyle enum member, but every member's value is a plain string ('always_double', 'always_single', 'prefer_double', 'prefer_single'), so passing the value instead of the member is an easy mistake — and it is accepted silently. Because _encode_quoted_str() compares quote_style against the members with in/==, a raw string matches none of them:

  • escaping decisions fall through to the "double-quote" branch while the delimiter check falls through to the single-quote branch, so quote_style='always_double' emits a single-quoted string with no quotes escaped;
  • quote_style='always_single' emits a single-quoted string in which single quotes are not escaped;
  • quote_style='prefer_double'/'prefer_single' silently never apply their preference (the flip/compare logic never matches).

The escaping guess and the delimiter can therefore contradict each other and produce output that json5.loads() itself rejects:

>>> json5.dumps("it's", quote_style='always_double')
"'it's'"
>>> json5.loads("'it's'")
ValueError: <string>:1 Unexpected "'" at column 5

Fix

In JSON5Encoder.__init__, coerce plain-string values into QuoteStyle members (QuoteStyle('always_double') → QuoteStyle.ALWAYS_DOUBLE) and raise a ValueError listing the valid values for strings that do not correspond to a member:

>>> json5.dumps("it's", quote_style='always_double')
'"it\'s"'
>>> json5.dumps("it's", quote_style='always-quote')
ValueError: 'always-quote' is not a valid QuoteStyle; expected one of: 'always_double', 'always_single', 'prefer_double', 'prefer_single'

This mirrors what json5/tool.py already does with its QUOTE_STYLES mapping, and leaves non-string custom values untouched for backwards compatibility.

Verification

  • New regression test test_quote_style_string_is_coerced asserts, for all four members, that passing member.value produces byte-identical output to passing member (including a string containing both quote kinds), that both 'always_double' and 'always_single' now round-trip through loads(), and that an invalid string raises ValueError.
  • Full local suite: python3 -m pytest tests/lib_test.py → 74 passed (73 pre-existing + 1 new).
  • Noted in passing: tests/lib_test.py.new and tests/lib_test.py.new.2 look like accidentally committed editor leftovers on main — happy to remove them in a separate cleanup PR if wanted.

Passing a QuoteStyle member's own value (e.g. 'always_double') instead of
the member itself would never match the 'in' checks against the members,
so strings were emitted with the wrong quote style, or worse, with raw
unescaped quotes that json5.loads() itself rejects:

    >>> json5.dumps("it's", quote_style='always_double')
    "'it's'"        # single-quoted, unescaped, cannot be parsed back

Coerce plain-string quote_style values into QuoteStyle members in
JSON5Encoder.__init__, and raise a ValueError listing the valid values
for strings that do not correspond to a member.

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>

@dpranke dpranke left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted in a comment on the bug, I think I'd rather have this just fail rather accept the coercion. Does that make sense, and do you feel like changing things accordingly?

…rings

Per review feedback, reject any value that is not a QuoteStyle member
(including strings that name or equal a member's value) instead of
coercing them, per the documented signature.

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
@rahul05ranjan

Copy link
Copy Markdown
Contributor Author

That makes sense — done in acdbaf2. The coercion is gone: any value that is not a QuoteStyle member now raises ValueError listing the valid members, including a string that names or equals a member's value. Since the parameter is typed as QuoteStyle, non-string values fail the same way rather than silently falling back to double-quote behavior; happy to relax that if you'd rather keep the old leniency for non-strings.

Comment thread json5/lib.py Outdated
self.trailing_commas = trailing_commas
self.allow_duplicate_keys = allow_duplicate_keys

# `quote_style` must be a `QuoteStyle` member. A plain string

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this comment; it's clear enough from the code what it's doing.

As for the error message, I appreciate the effort, but it's more detailed than I would like.
I'd probably use something like ValueError('quote_style argument must be a json5.QuoteStyle value'). We don't need to list each individual value

Comment thread tests/lib_test.py Outdated
# pass the value where the member itself is meant; that is a bug
# and must fail loudly rather than silently produce
# wrongly-quoted output.
with self.assertRaises(ValueError):

@dpranke dpranke Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these tests are more exhaustive than they need to be; just test that a json5.QuoteStyle value works and something that isn't a QuoteStyle doesn't. If you really want to be paranoid, you could have two failing tests, one for a 'always_double' and one for 1 or some other value.

You also don't need the tests on lines 1161-1168; working values are covered by other existing tests (specifically test_string_quote_styles.

So, I'd either move these tests inside that method, or create a new method that is immediately next to that method, and call it test_invalid_quote_styles; your name is more specific than it needs to be as well (since we might want to test for other non-strings).

@dpranke dpranke left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates! I have some comments about the wording and testing, but otherwise things are close :).

- Drop the explanatory comment and the valid-value listing; the error
  is now a plain 'must be a json5.QuoteStyle value'.
- Replace the exhaustive rejection tests with test_invalid_quote_styles,
  placed next to test_string_quote_styles, covering a member's value
  string ('always_double') and a non-string value (1).

Co-authored-by: rahul05ranjan <11793455+rahul05ranjan@users.noreply.github.com>
@rahul05ranjan

Copy link
Copy Markdown
Contributor Author

Thanks for the re-review! Addressed in d7f9f1c:

  • Dropped the comment and simplified the error message to ValueError('quote_style argument must be a json5.QuoteStyle value') without listing the individual values.
  • Replaced the rejection tests with a minimal test_invalid_quote_styles placed right next to test_string_quote_styles, covering the two failure cases you mentioned ('always_double' and 1); the working-value coverage is left to the existing tests.

Full suite passes locally (74/74).

@dpranke dpranke left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thank you!

@dpranke
dpranke merged commit ecc6413 into dpranke:main Nov 1, 2026
@rahul05ranjan

Copy link
Copy Markdown
Contributor Author

Appreciate the merge on #141 — nice to see "fix: coerce string quote_style values into QuoteStyle members" ship.

On a personal note: I'm on the lookout for my next role (Full Stack (PHP, NodeJs, Python) / Data Science / Applied AI / Devtools, based in India). Happy to chat if there's an opening on your side or one you'd recommend:
LinkedIn: https://www.linkedin.com/in/rahulswt7/ · GitHub: https://github.com/rahul05ranjan

@dpranke

dpranke commented Nov 1, 2026

Copy link
Copy Markdown
Owner

On a personal note: I'm on the lookout for my next role

Sure :). It's been nice working with you. I've got nothing for you at the moment, but I'll try to keep you in mind if I hear of anything.

@rahul05ranjan

Copy link
Copy Markdown
Contributor Author

Thanks Dirk, really appreciate it — and same here, this was a genuinely fun one to work on. Your reviews were quick, specific, and easy to act on, which made the whole loop a pleasure.

Zero pressure on the role front: if something crosses your radar I'd love a pointer, and if not, the collaboration was well worth it on its own. 🙂

All the best with pyjson5!

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.

quote_style passed as its own string value silently produces wrong or unparseable output

2 participants