====== PHP RFC: Opt-in JSON Comments and Trailing Commas ====== * Version: 1.0 * Date: 2026-10-13 * Author: Otar Chekurishvili, otar@hey.com * Status: Under Discussion * Implementation: https://github.com/php/php-src/pull/23336 * Discussion thread: https://news-web.php.net/php.internals/132868 * Voting thread: tbd ===== Introduction ===== **Write comments in your JSON. Keep the last comma. Strict JSON stays the default.** This RFC adds two opt-in flags for ''json_decode()'' and ''json_validate()'': ''JSON_ALLOW_COMMENTS'' and ''JSON_ALLOW_TRAILING_COMMAS''. Many tools use JSON for configuration files that people edit. In these files, people want to explain a setting with a comment. They also want to add an item to a list without a change to the line before it. Today, ''json_decode()'' rejects comments and trailing commas. Thus PHP projects use userland libraries or regular expressions to remove comments before they call ''json_decode()''. 'localhost', 'port' => 5432, 'options' => ['persistent', 'utf8']] $data = json_decode($config, true); // NULL, json_last_error_msg(): "Syntax error near location 2:5" Each flag enables one syntax feature. The two flags are independent: code can use one flag or both flags. Without the flags, ''json_decode()'' and ''json_validate()'' do not change. ===== Proposal ===== ==== New constants ==== This RFC adds two global constants to ext/json: The constants use bits 23 and 24. No other ''JSON_*'' flag uses these bits. ==== Changed functions ==== * ''json_decode()'' accepts the two flags in ''$flags''. * ''json_validate()'' accepts the two flags in ''$flags''. Before this RFC, it accepted only ''JSON_INVALID_UTF8_IGNORE''. Other flags still cause a ''ValueError''. The message of this ''ValueError'' now names all three accepted flags. * ''json_encode()'' does not change. It never writes comments or trailing commas. The two flags have no effect on ''json_encode()''. ==== JSON_ALLOW_COMMENTS ==== With this flag, the parser accepts two types of comments: * A line comment starts with ''%%//%%''. It ends at the end of the line or at the end of the input. * A block comment starts with ''%%/*%%'' and ends with ''%%*/%%''. It can contain line breaks. The parser discards each comment, so a comment has no effect on the result. These rules apply: * A comment can occur at each position where JSON permits whitespace: before a value, after a value, and between two tokens. * A comment cannot occur inside a token. For example, ''%%tr/**/ue%%'' and ''%%-/**/1%%'' cause a syntax error. A comment can directly follow a token, for example ''%%1.5/*note*/%%''. * Text inside a string is never a comment. ''%%"https://example.com/*"%%'' is a normal string. * A line comment ends at a line feed (LF), at a carriage return (CR), or at CR LF. U+2028 and U+2029 do not end a line comment. * Block comments do not nest. The first ''%%*/%%'' ends the comment. * The ''*'' in ''%%/*%%'' cannot also close the comment. Thus ''%%/*/1%%'' is an unterminated comment, and ''%%/**/1%%'' is the value 1. * The parser does not accept ''#'' comments. * A comment is not a value. Input with only comments causes a syntax error, the same as empty input. * A comment can contain all valid UTF-8 characters except NUL (U+0000). This includes the control characters U+0001 to U+001F. These characters cannot go into the result, because the parser discards the comment text. * A NUL byte in a comment causes ''JSON_ERROR_CTRL_CHAR'' at the position of the NUL byte, the same as a NUL byte outside a comment. ''JSON_INVALID_UTF8_IGNORE'' and ''JSON_INVALID_UTF8_SUBSTITUTE'' do not change this. Some other parsers end a comment at a NUL byte. The error prevents a different result in PHP for the same input. * Invalid UTF-8 in a comment causes ''JSON_ERROR_UTF8'', the same as in a string. With ''JSON_INVALID_UTF8_IGNORE'' or ''JSON_INVALID_UTF8_SUBSTITUTE'', the parser ignores invalid bytes in comments. No U+FFFD goes into the result, because the parser discards the comment text. * An unterminated block comment causes ''JSON_ERROR_SYNTAX''. The error location is the position of its ''%%/*%%''. * Error locations count the lines inside comments. As outside comments, only LF and CR LF start a new line. A CR alone ends a line comment, but it does not start a new line. Thus ''near location 3:5'' always points to the correct line of the original input. ==== JSON_ALLOW_TRAILING_COMMAS ==== With this flag, the parser accepts one comma after the last item of a non-empty array or object. In this RFC, an "item" is an array element or an object member. ^ Input ^ Result with JSON_ALLOW_TRAILING_COMMAS ^ | ''[1, 2, 3,]'' | ''[1, 2, 3]'' | | ''{"a": 1, "b": 2,}'' | ''{"a": 1, "b": 2}'' | | ''[,]'' and ''{,}'' | Syntax error: there is no item before the comma | | ''[1,,]'' and ''[1,,2]'' | Syntax error: more than one comma after an item | | ''1,'' | Syntax error: a top-level value cannot have a trailing comma | | ''[1,}'' | State mismatch error, the same as for ''[1}'' | Whitespace can occur between the trailing comma and the closing bracket. With ''JSON_ALLOW_COMMENTS'', comments can also occur there. ==== Behavior that does not change ==== * Without the new flags, the parser gives the same result, error code and error location as before. A differential test with 120,025 inputs found no difference (see [[#patches_and_tests|Patches and Tests]]). * The new flags do not change the result for valid strict JSON. * ''JSON_OBJECT_AS_ARRAY'', ''JSON_BIGINT_AS_STRING'', ''JSON_INVALID_UTF8_IGNORE'', ''JSON_INVALID_UTF8_SUBSTITUTE'' and ''JSON_THROW_ON_ERROR'' operate as before, also together with the new flags. * Comments and trailing commas do not change the nesting depth. The ''$depth'' argument operates as before. * For duplicate keys, the last value is the result, as before. * There are no new error codes. Without its flag, a comment or a trailing comma causes ''JSON_ERROR_SYNTAX'' at the same location as before. * A UTF-8 byte order mark (BOM) at the start of the input still causes a syntax error. ==== Examples ==== With only ''JSON_ALLOW_COMMENTS'', the trailing comma in ''$config'' from the introduction is still an error: var_dump(json_decode($config, true, flags: JSON_ALLOW_COMMENTS)); // NULL echo json_last_error_msg(); // Syntax error near location 8:5 A comment marker inside a string is not a comment. A regular expression that removes comments can damage this string: var_dump(json_decode('{"url": "https://example.com/*"}', true, flags: JSON_ALLOW_COMMENTS)); // array(1) { ["url"]=> string(21) "https://example.com/*" } For an unterminated block comment, the error location is the position of its ''%%/*%%'': var_dump(json_decode("[1,\n /* note", flags: JSON_ALLOW_COMMENTS)); // NULL echo json_last_error_msg(); // Syntax error near location 2:3 ''json_validate()'' accepts the same flags: var_dump(json_validate('{"a": 1, /* note */}', flags: JSON_ALLOW_COMMENTS | JSON_ALLOW_TRAILING_COMMAS)); // bool(true) ''JSON_THROW_ON_ERROR'' operates as before: try { json_decode('[1,,]', flags: JSON_ALLOW_TRAILING_COMMAS | JSON_THROW_ON_ERROR); } catch (JsonException $e) { echo $e->getMessage(); // Syntax error near location 1:4 } ==== Relation to JSON5, JSONC and JWCC ==== In the pre-RFC thread, Larry Garfield asked if this RFC adds JSON5. It does not. JSON5 is a much larger extension of JSON. It adds single-quoted strings, unquoted keys, hexadecimal numbers, ''Infinity'' and ''NaN'', a leading ''+'' sign, multi-line strings, and more whitespace characters. Some of these features change the data model, because strict JSON has no ''NaN'' and no ''Infinity''. The two flags of this RFC change only the syntax. If you remove all comments and trailing commas from the input, you get strict JSON with the same value. As Anton Smirnov wrote in the pre-RFC thread, the two flags are close to JSONC and to JWCC (JSON With Commas and Comments): ^ Syntax ^ Strict JSON ^ JSON_ALLOW_COMMENTS ^ Both flags ^ JSONC (draft) ^ JWCC ^ JSON5 ^ | ''%%//%%'' and ''%%/* */%%'' comments | No | Yes | Yes | Yes | Yes | Yes | | One trailing comma | No | No | Yes | Optional | Yes | Yes | | A CR alone ends a line comment | - | Yes | Yes | Yes | No | Yes | | U+2028 and U+2029 end a line comment | - | No | No | No | No | Yes | | Single quotes, unquoted keys, hexadecimal numbers, ''NaN'', ''Infinity'' | No | No | No | No | No | Yes | Thus ''JSON_ALLOW_COMMENTS'' accepts the syntax of the JSONC draft without trailing commas. Both flags together accept JWCC. Tailscale uses JWCC in Go with the name HuJSON. There is one small difference: in JWCC, only LF ends a line comment. In this RFC, a CR alone also ends a line comment, as in the JSONC draft. The flags do not bind PHP to JSON5 or to a JSONC specification. Each flag names one syntax feature with one exact meaning. Later RFCs can add more flags, one feature at a time. For the reasons against one ''JSON_ALLOW_JSONC'' flag, see [[#rejected_features|Rejected Features]]. ==== Prior art ==== Other JSON parsers also use one flag for each syntax feature: ^ Parser ^ Comments ^ Trailing commas ^ | Chromium ''base::JSONReader'' (C++) | ''JSON_ALLOW_COMMENTS'' | ''JSON_ALLOW_TRAILING_COMMAS'' | | RapidJSON (C++) | ''kParseCommentsFlag'' | ''kParseTrailingCommasFlag'' | | Jackson (Java) | ''JsonReadFeature.ALLOW_JAVA_COMMENTS'' | ''JsonReadFeature.ALLOW_TRAILING_COMMA'' | | .NET ''System.Text.Json'' | ''JsonCommentHandling.Skip'' | ''AllowTrailingCommas'' | | seld/jsonlint (PHP, used by Composer) | ''JsonParser::ALLOW_COMMENTS'' | - | Chromium uses the same two names as this RFC. ==== Demand ==== Many PHP projects already parse JSON with comments. Packagist data from 2026-10-12: * ''colinodell/json5'' (a JSON5 parser): 30.2 million installs, 1.76 million in the last month. For example, ''infection/infection'' uses it to read the ''infection.json5'' configuration file. * ''adhocore/json-comment'' (a comment remover): 737,000 installs. * ''seld/jsonlint'' added the ''JsonParser::ALLOW_COMMENTS'' flag in version 1.10.0 (2023). Many common configuration files contain comments, for example VS Code ''settings.json'', TypeScript ''tsconfig.json'' and Dev Container ''devcontainer.json''. PHP tools that read these files need a parser that accepts comments. ==== Why not in userland ==== A userland library can remove comments before it calls ''json_decode()''. This approach has problems: * The library must know JSON strings. A simple regular expression can damage strings such as ''%%"https://example.com/*"%%''. * After the library removes comments, the error locations do not match the original file. * The library copies the input and reads it two times. * ''json_validate()'' cannot check such files. The native scanner processes comments in one pass. It makes no copy, and it keeps the original error locations. ===== Backward Incompatible Changes ===== * The RFC adds two global constants. If code defines a global constant with the same name, PHP shows the warning "Constant JSON_ALLOW_COMMENTS already defined, this will be an error in PHP 9" and keeps the built-in value. This RFC does not affect constants in a namespace. A Sourcegraph code search on 2026-10-12 found no PHP code that uses these names. * Today, ''json_decode()'' ignores unknown flag bits. If code gives bit 23 or bit 24 by mistake, input with comments or trailing commas can now decode without an error. The result for valid strict JSON does not change. * The ''ValueError'' message of ''json_validate()'' for an invalid flag changes. It now names all three accepted flags. ===== Proposed PHP Version(s) ===== Next minor version: PHP 8.7. ===== RFC Impact ===== ==== To the Ecosystem ==== IDEs, language servers and static analyzers must add the two constants to their stubs, for example JetBrains phpstorm-stubs, PHPStan and Psalm. A library that also supports older PHP versions can check ''%%PHP_VERSION_ID >= 80700%%''. Then it can use the native flags and keep its userland parser as a fallback. ==== To Existing Extensions ==== Only ext/json changes. The internal C parser API (''php_json_parser_init()'') accepts the two new option bits. Other extensions that use this API see no change, unless they give the new bits. The PR adds the macros ''PHP_JSON_ALLOW_COMMENTS'' and ''PHP_JSON_ALLOW_TRAILING_COMMAS'' to the public header ''php_json.h''. The labeler bot of php-src marks each change to this header as "ABI break". The PR does not change a struct or a function signature. ==== To SAPIs ==== None. ==== Performance ==== I have measured CPU instructions with Callgrind (Valgrind 3.22, GCC 13.3, ''-O2'') on commit a4cd163 of the PR and its master base. The input was a pretty-printed JSON document of 7.6 MB with 20,000 objects. The count includes only the call of ''json_decode()'' or ''json_validate()''. ^ Call and input ^ master ^ This RFC ^ Change ^ | ''json_decode()'', strict JSON, no new flag | 354,777,244 | 360,436,080 | +1.60% | | ''json_validate()'', strict JSON, no new flag | 333,889,491 | 339,548,009 | +1.69% | | ''json_decode()'', strict JSON, both new flags | - | 360,436,080 | Same as without the flags | | ''json_decode()'', 10.8 MB with a line comment on each line, both new flags | - | 405,796,303 | - | All extra instructions are in the scanner. The Bison parser uses the same number of instructions as before. Two small scanner changes reduce the extra cost for strict JSON to +0.56% (''json_decode()'') and +0.59% (''json_validate()''). These changes do not change behavior: * The scanner keeps the line data of an open block comment in the ''php_json_scanner'' struct. The current code fills two local variables at the start of each ''php_json_scan()'' call. This change adds two fields to the struct. * After whitespace or a line break, the scanner jumps directly back to the JSON state. It does not go through the state dispatch again. Code review can add these changes before the merge. ===== Open Issues ===== None. ===== Unaffected PHP Functionality ===== * ''json_encode()'' and ''JsonSerializable''. * ''json_decode()'' and ''json_validate()'' without the new flags. * All current error codes and error messages. ===== Future Scope ===== * Later RFCs can add more opt-in flags for other syntax features, if users need them. Examples are single-quoted strings, unquoted keys and hexadecimal numbers. Each new flag needs its own RFC. This RFC does not propose them. * If a format such as JSONC gets a stable specification, a later RFC can add one constant that combines the flags for this format. ===== Voting Choices ===== This RFC has two primary votes, one for each flag. Each vote needs a 2/3 majority. The flags are independent, so the result of one vote does not affect the other vote. ---- * Yes * No * Abstain * Yes * No * Abstain ===== Patches and Tests ===== * Implementation: https://github.com/php/php-src/pull/23336 * The PR adds six test files to ''ext/json/tests'' and updates ''json_validate_002.phpt''. The tests cover comment positions, comment delimiters, malformed comments, NUL bytes and invalid UTF-8 in comments, trailing commas, other flags and error locations. * The JSON fuzzer (''sapi/fuzzer/fuzzer-json.c'') also runs with the new flags. Its dictionary (''sapi/fuzzer/json.dict'') contains the new tokens. * The PR adds UPGRADING notes for the two flags and the two constants. * Differential test: generated 120,025 inputs. The test decoded each input with ''json_decode()'' and five current flag sets, and validated it with ''json_validate()'' and two flag sets (840,175 checks). The results, error codes and error messages are the same on master and on this branch. * Superset test: 21,963 checks with valid strict JSON gave the same result with all three combinations of the new flags. ===== Implementation ===== After the vote, this section will give the PHP version, the commit links and the link to the PHP manual. ===== References ===== * Pre-RFC discussion: https://news-web.php.net/php.internals/132236 * RFC 8259, The JSON Data Interchange Format: https://www.rfc-editor.org/rfc/rfc8259 * JSONC specification (draft): https://jsonc.org/ * JWCC, JSON With Commas and Comments: https://nigeltao.github.io/blog/2021/json-with-commas-comments.html * JSON5 specification: https://spec.json5.org/ * Chromium ''base::JSONReader'' flags: https://chromium.googlesource.com/chromium/src/+/main/base/json/json_reader.h * RapidJSON parse flags: https://rapidjson.org/namespacerapidjson.html * Jackson ''JsonReadFeature'': https://fasterxml.github.io/jackson-core/javadoc/2.14/com/fasterxml/jackson/core/json/JsonReadFeature.html * .NET ''System.Text.Json'', comments and trailing commas: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/invalid-json * seld/jsonlint: https://github.com/Seldaek/jsonlint * Userland JSONC library by me: https://github.com/otar/jsonc * Related RFC, ''json_validate()'': https://wiki.php.net/rfc/json_validate ===== Rejected Features ===== ==== One JSON_ALLOW_JSONC flag ==== In the pre-RFC thread, Tim Düsterhus proposed one flag for comments and trailing commas together. His reasons were to reference a standard, if one exists, and to prevent a combinatorial explosion of flags. This RFC keeps two flags, for these reasons: * There is no stable JSONC standard. The jsonc.org specification is a draft that can change. A flag that names a format must follow that format when the format changes. * The JSONC draft makes trailing commas optional: "JSONC parsers MAY support trailing commas". Its reference parser, jsonc-parser, rejects trailing commas by default. Thus a ''JSON_ALLOW_JSONC'' flag cannot define trailing commas by reference to JSONC. * The name JSONC has more than one meaning. The JWCC article lists two different formats with the name JSONC. * Each flag has one exact meaning that does not change. Users can enable only the feature that they need. For example, a project can accept comments and keep strict commas. * Two flags give four combinations. The tests and the fuzzer cover all four combinations. Each new flag needs its own RFC, so the number of flags increases only after a vote. * Other JSON parsers also use separate flags (see [[#prior_art|Prior art]]). If a stable standard appears later, a new constant can combine the flags. This needs no change to the flags of this RFC. ==== Full JSON5 support ==== JSON5 adds many more features than comments and trailing commas. Some of them change the data model, for example ''NaN'' and ''Infinity''. Each of these features needs its own discussion. With the flag approach, a later RFC can add one feature at a time. ==== Hash comments ==== JavaScript, JSONC, JWCC and JSON5 do not have ''#'' comments. This RFC does not accept them. ==== Nested block comments ==== JavaScript and the JSONC draft do not nest block comments. This RFC uses the same rule: the first ''%%*/%%'' ends the comment. ==== More than one trailing comma ==== In JavaScript, ''[1,,2]'' contains an empty array slot. JSONC, JWCC and JSON5 accept only one trailing comma. This RFC rejects ''[1,,]'' and ''[1,,2]''. ==== Comment removal before parsing ==== A pre-processor can remove comments and then call the strict parser. This approach changes error locations, copies the input, and must know JSON strings. The native scanner has none of these problems. ===== Changelog ===== * 1.0 (2026-10-12): First version on the wiki. Changes after the pre-RFC thread and the first draft implementation: * The RFC explains the relation to JSON5, JSONC and JWCC. * The RFC gives the reasons for two flags, not one ''JSON_ALLOW_JSONC'' flag. * A NUL byte in a comment now causes ''JSON_ERROR_CTRL_CHAR''. The first draft accepted it as comment text. * A CR alone in a comment no longer starts a new line in error locations. * The RFC adds test results and performance measurements.