Skip to content

GH-51037: [C++] Replace RapidJSON with simdjson in JSON parser - #51038

Open
Reranko05 wants to merge 23 commits into
apache:mainfrom
Reranko05:gh-35460-parser
Open

Reranko05 wants to merge 23 commits into
apache:mainfrom
Reranko05:gh-35460-parser

Conversation

@Reranko05

@Reranko05 Reranko05 commented Aug 29, 2026 •

Copy link
Copy Markdown
Collaborator

Rationale for this change

This PR continues the simdjson migration by replacing the RapidJSON-based parsing implementation used by the JSON parser.

The existing parser uses RapidJSON's SAX/handler interface to parse JSON values and populate Arrow builders. This change replaces that implementation with simdjson's ondemand API while retaining the existing builder and type-inference logic.

Changes

  • Replace the RapidJSON parser and handler interface with simdjson's ondemand API.
  • Parse JSON documents using simdjson::ondemand::parser::iterate_many.
  • Use ResolveSimdjsonResult() consistently when handling simdjson results.
  • Preserve support for nested objects and arrays.
  • Preserve explicit-schema and inferred-field behavior.
  • Preserve unexpected-field handling for Error, Ignore, and InferType.
  • Continue storing numeric values as raw JSON tokens.
  • Trim trailing whitespace from numeric raw tokens to preserve existing behavior.
  • Preserve JSON parse error propagation through Status::Invalid.
  • Remove the parser's RapidJSON-specific dependencies.

Are there any user-facing changes?

No

Was AI used for this PR?

In accordance to the AI generation guidelines, please disclose below whether and how AI was used in this PR.

PR code and description written by:

  • Human
  • AI

Reviewed before submission by:

  • Human
  • AI
  • Not reviewed

Fixes: #51037

@Reranko05 Reranko05 added the CI: Extra: C++ Run extra C++ CI label Aug 29, 2026
@Reranko05
Reranko05 force-pushed the gh-35460-parser branch 3 times, most recently from d02c90a to d9c6d74 Compare August 30, 2026 05:57
@Reranko05
Reranko05 marked this pull request as ready for review August 30, 2026 06:34
Copilot AI lite review requested due to automatic review settings August 30, 2026 06:34
@Reranko05
Reranko05 requested review from pitrou and rok as code owners August 30, 2026 06:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Reranko05
Reranko05 marked this pull request as draft September 4, 2026 11:31
@Reranko05

Reranko05 commented Sep 7, 2026 •

Copy link
Copy Markdown
Collaborator Author

@pitrou @kou Should we use padded_string_view with a reusable buffer here as well, similar to the chunker?

@Reranko05

Copy link
Copy Markdown
Collaborator Author

@github-actions crossbow submit -g cpp

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown

Revision: a4dea4b

Submitted crossbow builds: ursacomputing/crossbow @ actions-9fde492db2

Task Status
example-cpp-minimal-build-static GitHub Actions
example-cpp-minimal-build-static-system-dependency GitHub Actions
example-cpp-tutorial GitHub Actions
test-build-cpp-fuzz GitHub Actions
test-conda-cpp GitHub Actions
test-conda-cpp-valgrind GitHub Actions
test-debian-13-cpp-amd64 GitHub Actions
test-debian-13-cpp-i386 GitHub Actions
test-debian-experimental-cpp-gcc-15 GitHub Actions
test-fedora-42-cpp GitHub Actions
test-ubuntu-22.04-cpp GitHub Actions
test-ubuntu-22.04-cpp-bundled GitHub Actions
test-ubuntu-22.04-cpp-emscripten GitHub Actions
test-ubuntu-22.04-cpp-no-threading GitHub Actions
test-ubuntu-24.04-cpp GitHub Actions
test-ubuntu-24.04-cpp-bundled-offline GitHub Actions
test-ubuntu-24.04-cpp-gcc-13-bundled GitHub Actions
test-ubuntu-24.04-cpp-gcc-14 GitHub Actions
test-ubuntu-24.04-cpp-minimal-with-formats GitHub Actions
test-ubuntu-24.04-cpp-thread-sanitizer GitHub Actions

@pitrou

pitrou commented Sep 7, 2026

Copy link
Copy Markdown
Member

@pitrou @kou Should we use padded_string_view with a reusable buffer here as well, similar to the chunker?

We should check the Buffer capacity first to see if it has enough padding already.

@Reranko05

Copy link
Copy Markdown
Collaborator Author

@github-actions crossbow submit -g cpp

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

Revision: 713496d

Submitted crossbow builds: ursacomputing/crossbow @ actions-884a0df4e2

Task Status
example-cpp-minimal-build-static GitHub Actions
example-cpp-minimal-build-static-system-dependency GitHub Actions
example-cpp-tutorial GitHub Actions
test-build-cpp-fuzz GitHub Actions
test-conda-cpp GitHub Actions
test-conda-cpp-valgrind GitHub Actions
test-debian-13-cpp-amd64 GitHub Actions
test-debian-13-cpp-i386 GitHub Actions
test-debian-experimental-cpp-gcc-15 GitHub Actions
test-fedora-42-cpp GitHub Actions
test-ubuntu-22.04-cpp GitHub Actions
test-ubuntu-22.04-cpp-bundled GitHub Actions
test-ubuntu-22.04-cpp-emscripten GitHub Actions
test-ubuntu-22.04-cpp-no-threading GitHub Actions
test-ubuntu-24.04-cpp GitHub Actions
test-ubuntu-24.04-cpp-bundled-offline GitHub Actions
test-ubuntu-24.04-cpp-gcc-13-bundled GitHub Actions
test-ubuntu-24.04-cpp-gcc-14 GitHub Actions
test-ubuntu-24.04-cpp-minimal-with-formats GitHub Actions
test-ubuntu-24.04-cpp-thread-sanitizer GitHub Actions

@Reranko05
Reranko05 marked this pull request as ready for review September 8, 2026 14:41
Copilot AI review requested due to automatic review settings September 8, 2026 14:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Reranko05

Copy link
Copy Markdown
Collaborator Author

@pitrou @rok I’ve rebased the parser PR and incorporated the latest feedback, including reusing the simdjson::ondemand::parser and using padded_string_view when the Arrow Buffer has sufficient capacity.

The native C++ build and tests pass, but Crossbow’s ubuntu-cpp-emscripten job now fails in arrow-dataset-file-json-test:

RuntimeError: Aborted(). Build with -sASSERTIONS for more info.
    at abort (/build/cpp/debug/arrow-dataset-file-json-test.js:491:11)
    at _abort (/build/cpp/debug/arrow-dataset-file-json-test.js:4483:7)
    at invoke_v (/build/cpp/debug/arrow-dataset-file-json-test.js:5907:29)
    at arrow-dataset-file-json-test.wasm.std::__terminate(void (*)()) (wasm://wasm/arrow-dataset-file-json-test.wasm-0f1ca1f2:wasm-function[41574]:0x277e6cb)
    at arrow-dataset-file-json-test.wasm.std::terminate() (wasm://wasm/arrow-dataset-file-json-test.wasm-0f1ca1f2:wasm-function[41572]:0x277e6a4)
    at arrow-dataset-file-json-test.wasm.simdjson::fallback::ondemand::document_stream::start() (wasm://wasm/arrow-dataset-file-json-test.wasm-0f1ca1f2:wasm-function[20092]:0x10168d0)
    at arrow-dataset-file-json-test.wasm.arrow::Status arrow::Status arrow::json::HandlerBase::DoParse<arrow::json::Handler<(arrow::json::UnexpectedFieldBehavior)2>>(arrow::json::Handler<(arrow::json::UnexpectedFieldBehavior)2>&, std::__2::shared_ptr<arrow::Buffer> const&)::'lambda'(arrow::json::Handler<(arrow::json::UnexpectedFieldBehavior)2> const&)::operator()<simdjson::padded_string>(arrow::json::Handler<(arrow::json::UnexpectedFieldBehavior)2> const&) const (wasm://wasm/arrow-dataset-file-json-test.wasm-0f1ca1f2:wasm-function[20180]:0x1030c99)
    at invoke_viii (/build/cpp/debug/arrow-dataset-file-json-test.js:5885:29)
    at arrow-dataset-file-json-test.wasm.arrow::Status arrow::json::HandlerBase::DoParse<arrow::json::Handler<(arrow::json::UnexpectedFieldBehavior)2>>(arrow::json::Handler<(arrow::json::UnexpectedFieldBehavior)2>&, std::__2::shared_ptr<arrow::Buffer> const&) (wasm://wasm/arrow-dataset-file-json-test.wasm-0f1ca1f2:wasm-function[20178]:0x102f33c)
    at arrow-dataset-file-json-test.wasm.arrow::json::Handler<(arrow::json::UnexpectedFieldBehavior)2>::Parse(std::__2::shared_ptr<arrow::Buffer> const&) (wasm://wasm/arrow-dataset-file-json-test.wasm-0f1ca1f2:wasm-function[20177]:0x102ef89)

Do you have any idea what I need to do to fix this?

@pitrou

pitrou commented Sep 8, 2026

Copy link
Copy Markdown
Member

@Reranko05 No idea without taking a deeper look :-) But I'd like us to merge the chunker PR first and then come back to this one.

@Reranko05

Copy link
Copy Markdown
Collaborator Author

@pitrou Okay, will wait until chunker is merged.

@Reranko05
Reranko05 requested a review from pitrou September 22, 2026 16:40
Comment thread cpp/src/arrow/json/parser.cc Outdated
Comment on lines +783 to +784
if (stream.truncated_bytes() != 0) {
return ParseError("The document is empty");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't understand this error message. The doc says:

If truncated_bytes() differs from zero, then the input was truncated maybe because incomplete JSON documents were found at the end of the stream.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I initially associated it with the empty-document handling from the previous implementation. I have updated the message.

Comment on lines +754 to +756
if (internal::ConsumeJsonWhitespace(input, /*trailing=*/false) == input_size) {
return Status::OK();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

As replied in another comment, let's find out if this is really necessary.

Comment thread cpp/src/arrow/json/parser.cc Outdated
Comment thread cpp/src/arrow/json/parser.cc Outdated
Comment thread cpp/src/arrow/json/parser.cc Outdated
Comment thread cpp/src/arrow/json/parser_test.cc Outdated
Comment on lines +330 to +331
ASSERT_RAISES(Invalid,
ParseFromString(options, R"({"known": 1, "ignored": [1,]})", &parsed));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks. Can you also test the error message?

Comment thread cpp/src/arrow/json/parser.cc
Comment thread cpp/src/arrow/json/reader_test.cc Outdated

/// Returns the number of leading whitespace characters when trailing is false,
/// or the number of trailing whitespace characters when trailing is true.
// XXX We could try to SIMD-accelerate this routine.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The original comment says this, why modify it?

// XXX We could try to SIMD-accelerate this routine but it's called only
// once per chunk and also will presumably examine a minimal amount of bytes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I removed that part because the helper is now also used by the parser, where it may examine more bytes than the original chunker-only use case. If that assumption is still valid, I can restore the comment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I removed that part because the helper is now also used by the parser, where it may examine more bytes than the original chunker-only use case. If that assumption is still valid, I can restore the comment.

Well, can you investigate to find out whether it still applies?

int64_t ConsumeJsonWhitespace(std::string_view view, bool trailing) {
if (!trailing) {
const auto pos = view.find_first_not_of(" \t\r\n");
return static_cast<int64_t>(pos == std::string_view::npos ? view.size() : pos);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The docstring says "Returns the number of leading whitespace characters", but this actually returns the view size (not 0) when there is no leading whitespace. Why?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Okay, my bad I got confused. I changed it to

/// Returns the position of the first non-whitespace character when trailing is false,
/// or the number of trailing whitespace characters when trailing is true.

is this fine?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So it's returning the number of leading/trailing whitespace characters in both cases? Perhaps say it like that to make it appear less confusing?

@pitrou

pitrou commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

@Reranko05 Can you please follow the PR template for your PR description?

Copilot AI review requested due to automatic review settings September 23, 2026 17:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings September 23, 2026 17:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Reranko05

Reranko05 commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator Author

@pitrou Addressed most of the comments. I will now investigate the ConsumeJsonWhitespace() comment and the test pass/fail inconsistency.

BTW, thanks for the reviews. I know you already have a lot of work on your plate, so I really appreciate you taking the time to go through all of this feedback. I am learning a lot from it :)

@Reranko05

Copy link
Copy Markdown
Collaborator Author

@pitrou I investigated this further. When the ConsumeJsonWhitespace() guard is removed, BlockParserWithSchema.Empty passes when run alone but fails in the full suite.

In both cases, iterate_many() returns SUCCESS. This means SIMDJSON was able to create the document stream successfully and did not report an error while creating it. The stream then contains 0 documents because the input is empty.

The difference comes afterward: truncated_bytes() returns 0 when the test runs alone, but 4294967289 in the full suite, causing it to report empty input as truncated.

I also tried a fresh simdjson::ondemand::parser and disabled SIMDJSON threading, but the same discrepancy remained.

The ConsumeJsonWhitespace() guard avoids sending empty input to iterate_many(), and the full suite passes. What do you think is it fine to keep the check?

Also regarding the ConsumeJsonWhitespace() comment, I think this would be more accurate now:

// XXX We could try to SIMD-accelerate this routine, but it is called only
// a few times per input and is expected to examine a small amount of data.

@pitrou

pitrou commented Sep 28, 2026

Copy link
Copy Markdown
Member

The difference comes afterward: truncated_bytes() returns 0 when the test runs alone, but 4294967289 in the full suite, causing it to report empty input as truncated.

I see. Perhaps truncated_bytes is not supported on a stream that contains no complete document? Unfortunately the docstring is not very clear about it.

Can you perhaps open an issue on https://github.com/simdjson/simdjson/issues so that we can more insight on this?

@Reranko05

Copy link
Copy Markdown
Collaborator Author

@pitrou I was going to open an issue on simdjson, but their issue template asks for the problem to be reproducible with a standalone program using the vendored simdjson source. I tried that, but couldn't reproduce the behavior outside of Arrow, so I don't think I have enough to file an issue yet.

Within Arrow, I narrowed it down to InferringChunkedArrayBuilder.MultipleChunkIntegerParallel. I tried different iteration counts: 16 iterations passes, while 32 and higher trigger the failure in the subsequent empty parse. I also changed the test to use TaskGroup::MakeSerial() instead of the threaded task group, and the failure still occurs, so it doesn't appear to depend on the task group threading. I am confused on how to proceed now?

Comment on lines +2840 to +2842
# Keep simdjson's threading configuration consistent with Arrow's,
# which is required for Emscripten where Arrow threading is disabled.
set(SIMDJSON_ENABLE_THREADS ${ARROW_ENABLE_THREADING})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

https://github.com/simdjson/simdjson/blob/master/doc/iterate_many.md

Thread support is only active if thread supported is detected in which case the macro SIMDJSON_THREADS_ENABLED is set. You can also manually pass SIMDJSON_THREADS_ENABLED=1 flag to the library. Otherwise the library runs in single-thread mode.

Note the code section for this auto-detection:

// Is threading enabled?
#if defined(_REENTRANT) || defined(_MT)
#ifndef SIMDJSON_THREADS_ENABLED
#define SIMDJSON_THREADS_ENABLED
#endif
#endif

So even if we do not set the cmake option SIMDJSON_ENABLE_THREADS, we can still get threading enabled

Comment on lines +783 to +785
if (stream.truncated_bytes() != 0) {
return ParseError("JSON document was truncated");
}

@taepper taepper Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think truncated_bytes can also have problems when we have partial utf-8 characters at the end of the input.

Also conceptually, we did not use truncated_bytes in the json chunker. It is now not obvious to me why we use simdjson's api in a different way for the parser. To me this will make the code more brittle and we should be consistent with our api usage.

Note that we could replace the FindLast method in the chunker with a single call to truncated_bytes(). But this can lead to some weird behavior with partial utf-8 characters at the end of the stream. Also, as @pitrou already identified, the api contract of truncated_bytes is a little unclear / difficult to reason about

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also conceptually, we did not use truncated_bytes in the json chunker. It is now not obvious to me why we use simdjson's api in a different way for the parser. To me this will make the code more brittle and we should be consistent with our api usage.

Do you want to experiment with this @Reranko05 ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Sure, I will try.

@pitrou

pitrou commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Within Arrow, I narrowed it down to InferringChunkedArrayBuilder.MultipleChunkIntegerParallel. I tried different iteration counts: 16 iterations passes, while 32 and higher trigger the failure in the subsequent empty parse. I also changed the test to use TaskGroup::MakeSerial() instead of the threaded task group, and the failure still occurs, so it doesn't appear to depend on the task group threading. I am confused on how to proceed now?

This is what the docstring says, which I think explains the fluctuating behaviour you're seeing:
"""this value is only meaningful under the conditions below. It is computed from stage-1 bookkeeping, and outside these conditions it is not merely imprecise, it is arbitrary – it can exceed size_in_bytes() or wrap around to a huge value"""

I was going to open an issue on simdjson, but their issue template asks for the problem to be reproducible with a standalone program using the vendored simdjson source.

That's if you open an issue in the "bug" category, but I think you can use the more general "standard issue" template and let them decide how they feel about it.

@pitrou

pitrou commented Sep 28, 2026

Copy link
Copy Markdown
Member

You need to merge/rebase and fix conflicts @Reranko05 , btw.

Copilot AI review requested due to automatic review settings September 28, 2026 16:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings September 28, 2026 16:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Reranko05

Copy link
Copy Markdown
Collaborator Author

@pitrou rebased :)

@Reranko05

Copy link
Copy Markdown
Collaborator Author

Before i create an issue just to verify I am wording it correctly:-

Title : truncated_bytes() returns arbitrary values for empty streams

Desc :

### Description

We are using `simdjson::ondemand::document_stream` in Apache Arrow's JSON parser.

When the input is empty and the stream contains no documents, `truncated_bytes()` can return a large arbitrary value such as `4294967295`, causing empty input to be reported as truncated.

The behavior only occurs in the Arrow environment after repeated parser operations.

### Question

Is `truncated_bytes()` supported when the stream contains no complete documents? If not, could this limitation be made more explicit in the documentation?

Context: https://github.com/apache/arrow/pull/51038

@pitrou is this fine?

@pitrou

pitrou commented Sep 29, 2026

Copy link
Copy Markdown
Member

@Reranko05 Yes, that seems good to me.

Comment thread cpp/src/arrow/json/parser.cc Outdated
Copilot AI review requested due to automatic review settings September 29, 2026 11:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[C++] Replace RapidJSON with simdjson in JSON parser

7 participants