Repository navigation
Conversation
Define the character classes and repertoire masks used by the C, Java, and Ruby implementations, including strict UTF-8 decoding and replacement helpers. Package the Java helper in both extension jars. Verify the Ruby reference against the RFC ranges for every code point, with boundary and malformed UTF-8 maximal-subpart coverage.
Add unicode_subset and on_invalid_char options to the C and JRuby parsers and forward them through Coder. Validate decoded keys and values, report source positions and json_path, and support maximal-subpart replacement and unpaired surrogate escapes. Preserve the default behavior. Retain validation provenance for cached keys and check resumable chunk boundaries. Include shared parser tests and document the parsing options.
Validate or replace prohibited characters in C, JRuby, and TruffleRuby generation, including State and Coder. Preserve invalid_object, detect replacement key collisions, and reject replacement with RFC 8785. Fuse C validation with escaping, retain IO snapshots across callbacks, and keep normalization off the common path. Preserve default-path speed with compact numeric dispatch and direct output for single-digit integers. Include generator and API integration tests, Ractor coverage, and docs. Keep TruffleRuby's no-argument Object#to_json fallback working. On CRuby 4.0.6 arm64 macOS versus cdcfdb8, all 11 encoder fixtures remain within 1% regression for JSON.generate and Coder. Nine paired rounds put default Twitter generation at -0.31% and assignables overhead at 12.34%.
ydah
force-pushed
the
rfc9839-unicode-subset
branch
from
October 12, 2026 15:18
66de252 to
6e9d4ba
Compare
Member
Do you need this in one of your application? Isn't it simpler and faster to apply this restriction on the whole JSON payload prior to parsing? |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
JSON strings can contain escaped NUL, legacy controls, and noncharacters even when the document is syntactically valid. Applications whose protocols restrict these characters currently need their own checks across nested keys and values. RFC 9839 §5 describes applying Unicode repertoire restrictions to JSON data, including characters represented by escapes.
Changes
This adds
unicode_subsetandon_invalid_charoptions to parsing and generation. Applications can enforce the same policy on incoming and outgoing data, report violations at the JSON boundary, or explicitly replace prohibited characters. The defaults,unicode_subset: nilandon_invalid_char: :raise, preserve existing behavior.The repertoires follow RFC 9839 §4. All three allow tab, LF, CR, unassigned code points, and private-use code points.
Usage
Validation and edge cases
Validation covers object keys and string values after escape decoding and before output escaping. With a subset selected, replacement handles malformed UTF-8 by maximal subpart and preserves input strings. Distinct keys can become equal after replacement; leave
allow_duplicate_key: false(the default) to reject these collisions.With no subset selected,
:replaceonly relaxes parsing of unpaired surrogate escapes; it does not enable general character replacement. Generation rejects the combination ofon_invalid_char: :replaceandrfc8785: true. RawJSON::Fragmentcontents and customto_jsonoutput remain the caller's responsibility.Performance
Benchmarks: Measured on CRuby 4.0.6, arm64 macOS, using
benchmark/data/twitter.json(631,514 bytes). The baseline iscdcfdb8; the implementation is66de252.Each sample measures 1,000 operations after 300 warmup iterations. File loading, input preparation, and
Coderconstruction are excluded. Execution order alternates between rounds, withGC.startbefore each sample.The
:assignablesmeasurements use the defaulton_invalid_char: :raiseon input that satisfies the repertoire. Replacement and error handling are not timed.Default behavior
Default behavior:
JSON.parseJSON.generateJSON::Coder#dumpUnicode repertoire validation
Enabling
unicode_subset: :assignables::assignables(s / 1,000 calls)JSON.parseJSON.generateJSON::Coder#dumpMeasurement methodology
Times are medians of elapsed seconds per 1,000 operations, using every sample in each reported comparison. Percentages are calculated as
(after_median / before_median - 1) * 100, using the unrounded medians; negative values mean faster execution. Parsing uses 72 paired rounds (eight runs of nine); generation uses nine. The enabledCoder#dumpcomparison was measured in a separate nine-round run, which accounts for its different default timing.Benchmark reproduction
Benchmark code: baseline/default comparison and JSON.generate validation
Build both checkouts with the same Ruby and save these two files together.
worker.rb:compare.py:Run eight sets of nine rounds for parsing and one set for generation:
export RUBY=/path/to/ruby python3 compare.py \ /path/to/baseline /path/to/branch /path/to/twitter.json parse 8 python3 compare.py \ /path/to/baseline /path/to/branch /path/to/twitter.json generate 1Benchmark code: additional Coder#dump validation comparison
coder.rb: