This is an automated email from the ASF dual-hosted git repository.

pitrou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/main by this push:
     new bbbc06f8a86 GH-51037: [C++] Replace RapidJSON with simdjson in JSON 
parser (#51038)
bbbc06f8a86 is described below

commit bbbc06f8a8610fb6dbfdc8fe90da6315ec354ef6
Author: Aaditya Srinivasan <[email protected]>
AuthorDate: Tue Oct 6 17:48:35 2026 +0530

    GH-51037: [C++] Replace RapidJSON with simdjson in JSON parser (#51038)
    
    ### 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](https://arrow.apache.org/docs/dev/developers/overview.html#ai-generated-code),
 please disclose below whether and how AI was used in this PR.
    
    **PR code and description written by:**
    
    - [X] Human
    - [X] AI
    
    **Reviewed before submission by:**
    
    - [X] Human
    - [X] AI
    - [ ] Not reviewed
    
    Fixes: #51037
    
    * GitHub Issue: #51037
    
    Authored-by: Aaditya Srinivasan <[email protected]>
    Signed-off-by: Antoine Pitrou <[email protected]>
---
 cpp/cmake_modules/ThirdpartyToolchain.cmake |   4 +
 cpp/src/arrow/json/chunker.cc               |  13 +-
 cpp/src/arrow/json/parser.cc                | 648 +++++++++++-----------------
 cpp/src/arrow/json/parser_test.cc           |  36 +-
 cpp/src/arrow/json/reader_test.cc           |  33 +-
 cpp/src/arrow/util/simdjson_internal.cc     |  14 +
 cpp/src/arrow/util/simdjson_internal.h      |   4 +-
 python/pyarrow/tests/test_json.py           |   7 +-
 8 files changed, 346 insertions(+), 413 deletions(-)

diff --git a/cpp/cmake_modules/ThirdpartyToolchain.cmake 
b/cpp/cmake_modules/ThirdpartyToolchain.cmake
index db4482f7378..42da1eb425f 100644
--- a/cpp/cmake_modules/ThirdpartyToolchain.cmake
+++ b/cpp/cmake_modules/ThirdpartyToolchain.cmake
@@ -2869,6 +2869,10 @@ function(build_simdjson)
 
   prepare_fetchcontent()
 
+  # 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})
+
   # simdjson enables precompiled headers unconditionally.
   # Recompiling simdjson.cpp against it produces differing artifacts
   # Disable precompiled headers to avoid reproducible build failures.
diff --git a/cpp/src/arrow/json/chunker.cc b/cpp/src/arrow/json/chunker.cc
index c51c9213f93..c1397ba876c 100644
--- a/cpp/src/arrow/json/chunker.cc
+++ b/cpp/src/arrow/json/chunker.cc
@@ -32,16 +32,6 @@ namespace arrow {
 namespace json {
 namespace {
 
-// 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.
-int64_t ConsumeWhitespace(std::string_view view) {
-  const auto ws_count = view.find_first_not_of(" \t\r\n");
-  if (ws_count == std::string_view::npos) {
-    return view.size();
-  }
-  return static_cast<int64_t>(ws_count);
-}
-
 // A BoundaryFinder implementation that assumes JSON objects can contain raw 
newlines,
 // and uses the structural indexes computed by simdjson to delimit them.
 class ParsingBoundaryFinder : public BoundaryFinder {
@@ -156,7 +146,8 @@ class ParsingBoundaryFinder : public BoundaryFinder {
     if (consumed_length > 0) {
       // If we found at least one document, also consume its trailing 
whitespace
       // to avoid stray bytes at the end of the stream.
-      consumed_length += ConsumeWhitespace(input.substr(consumed_length));
+      consumed_length += 
internal::ConsumeJsonWhitespace(input.substr(consumed_length),
+                                                         /*trailing=*/false);
     }
     return consumed_length;
   }
diff --git a/cpp/src/arrow/json/parser.cc b/cpp/src/arrow/json/parser.cc
index 53f856d8012..2d2363fe431 100644
--- a/cpp/src/arrow/json/parser.cc
+++ b/cpp/src/arrow/json/parser.cc
@@ -27,10 +27,6 @@
 #include <utility>
 #include <vector>
 
-#include "arrow/json/rapidjson_defs.h"
-#include "rapidjson/error/en.h"
-#include "rapidjson/reader.h"
-
 #include "arrow/array.h"
 #include "arrow/array/builder_binary.h"
 #include "arrow/buffer_builder.h"
@@ -38,6 +34,7 @@
 #include "arrow/util/bitset_stack_internal.h"
 #include "arrow/util/checked_cast.h"
 #include "arrow/util/logging_internal.h"
+#include "arrow/util/simdjson_internal.h"
 #include "arrow/util/trie_internal.h"
 #include "arrow/visit_type_inline.h"
 
@@ -48,7 +45,7 @@ using internal::checked_cast;
 
 namespace json {
 
-namespace rj = arrow::rapidjson;
+namespace sj = simdjson::ondemand;
 
 template <typename... T>
 static Status ParseError(T&&... t) {
@@ -130,7 +127,7 @@ class RawArrayBuilder;
 
 /// \brief packed pointer to a RawArrayBuilder
 ///
-/// RawArrayBuilders are stored in HandlerBase,
+/// RawArrayBuilders are stored in ParseImpl,
 /// which allows storage of their indices (uint32_t) instead of a full pointer.
 /// BuilderPtr is also tagged with the json kind and nullable properties
 /// so those can be accessed before dereferencing the builder.
@@ -644,14 +641,12 @@ class RawBuilderSet {
       arenas_;
 };
 
-/// Three implementations are provided for BlockParser, one for each
-/// UnexpectedFieldBehavior. However most of the logic is identical in each
-/// case, so the majority of the implementation is in this base class
-class HandlerBase : public BlockParser,
-                    public rj::BaseReaderHandler<rj::UTF8<>, HandlerBase> {
+/// Parser implementation for BlockParser.
+class ParseImpl : public BlockParser {
  public:
-  explicit HandlerBase(MemoryPool* pool)
+  explicit ParseImpl(MemoryPool* pool, UnexpectedFieldBehavior 
unexpected_field_behavior)
       : BlockParser(pool),
+        unexpected_field_behavior_(unexpected_field_behavior),
         builder_set_(pool),
         field_index_(-1),
         scalar_values_builder_(pool) {}
@@ -662,71 +657,59 @@ class HandlerBase : public BlockParser,
     return builder_set_.Cast<kind>(builder);
   }
 
-  /// Accessor for a stored error Status
-  Status Error() { return status_; }
-
-  /// \defgroup rapidjson-handler-interface functions expected by rj::Reader
-  ///
-  /// bool Key(const char* data, rj::SizeType size, ...) is omitted since
-  /// the behavior varies greatly between UnexpectedFieldBehaviors
-  ///
-  /// @{
-  bool Null() {
-    status_ = builder_set_.AppendNull(builder_stack_.back(), field_index_, 
builder_);
-    return status_.ok();
+  Status Null() {
+    DCHECK(!builder_stack_.empty());
+    return builder_set_.AppendNull(builder_stack_.back(), field_index_, 
builder_);
+  }
+
+  Status HandleUnexpectedField(std::string_view key, sj::value value) {
+    switch (unexpected_field_behavior_) {
+      case UnexpectedFieldBehavior::Error:
+        return ParseError("unexpected field");
+
+      case UnexpectedFieldBehavior::Ignore:
+        return internal::ConsumeJsonValue(value);
+
+      case UnexpectedFieldBehavior::InferType: {
+        // If an unexpected field is encountered, add a NullBuilder with 
leading nulls.
+        // The next value parsed will promote this field to its inferred type.
+        auto struct_builder = Cast<Kind::kObject>(builder_stack_.back());
+        auto leading_nulls = static_cast<uint32_t>(struct_builder->length() - 
1);
+
+        builder_ = BuilderPtr(Kind::kNull, leading_nulls, /*nullable=*/true);
+        field_index_ = struct_builder->AddField(key, builder_);
+
+        return ParseValue(value);
+      }
+    }
+
+    return Status::OK();
   }
 
-  bool Bool(bool value) {
+  Status Bool(bool value) {
     constexpr auto kind = Kind::kBoolean;
     if (ARROW_PREDICT_FALSE(builder_.kind != kind)) {
-      status_ = IllegallyChangedTo(kind);
-      return status_.ok();
+      return IllegallyChangedTo(kind);
     }
-    status_ = Cast<kind>(builder_)->Append(value);
-    return status_.ok();
+    return Cast<kind>(builder_)->Append(value);
   }
 
-  bool RawNumber(const char* data, rj::SizeType size, ...) {
+  Status RawNumber(std::string_view value) {
     if (builder_.kind == Kind::kNumberOrString) {
-      status_ =
-          AppendScalar<Kind::kNumberOrString>(builder_, std::string_view(data, 
size));
+      return AppendScalar<Kind::kNumberOrString>(builder_, value);
     } else {
-      status_ = AppendScalar<Kind::kNumber>(builder_, std::string_view(data, 
size));
+      return AppendScalar<Kind::kNumber>(builder_, value);
     }
-    return status_.ok();
   }
 
-  bool String(const char* data, rj::SizeType size, ...) {
+  Status String(std::string_view value) {
     if (builder_.kind == Kind::kNumberOrString) {
-      status_ =
-          AppendScalar<Kind::kNumberOrString>(builder_, std::string_view(data, 
size));
+      return AppendScalar<Kind::kNumberOrString>(builder_, value);
     } else {
-      status_ = AppendScalar<Kind::kString>(builder_, std::string_view(data, 
size));
+      return AppendScalar<Kind::kString>(builder_, value);
     }
-    return status_.ok();
-  }
-
-  bool StartObject() {
-    status_ = StartObjectImpl();
-    return status_.ok();
-  }
-
-  bool EndObject(...) {
-    status_ = EndObjectImpl();
-    return status_.ok();
   }
 
-  bool StartArray() {
-    status_ = StartArrayImpl();
-    return status_.ok();
-  }
-
-  bool EndArray(rj::SizeType size) {
-    status_ = EndArrayImpl(size);
-    return status_.ok();
-  }
-  /// @}
-
   /// \brief Set up builders using an expected Schema
   Status Initialize(const std::shared_ptr<Schema>& s) {
     auto type = struct_({});
@@ -762,102 +745,207 @@ class HandlerBase : public BlockParser,
   }
 
  protected:
-  template <typename Handler, typename Stream>
-  Status DoParse(Handler& handler, Stream&& json, size_t json_size) {
-    constexpr auto parse_flags = rj::kParseIterativeFlag | 
rj::kParseNanAndInfFlag |
-                                 rj::kParseStopWhenDoneFlag |
-                                 rj::kParseNumbersAsStringsFlag;
-
-    rj::Reader reader;
-    // ensure that the loop can exit when the block too large.
-    for (; num_rows_ < std::numeric_limits<int32_t>::max(); ++num_rows_) {
-      auto ok = reader.Parse<parse_flags>(json, handler);
-      switch (ok.Code()) {
-        case rj::kParseErrorNone:
-          // parse the next object
-          continue;
-        case rj::kParseErrorDocumentEmpty:
-          if (json.Tell() < json_size) {
-            return ParseError(rj::GetParseError_En(ok.Code()));
-          }
-          // parsed all objects, finish
-          return Status::OK();
-        case rj::kParseErrorTermination:
-          // handler emitted an error
-          return handler.Error();
-        default:
-          // rj emitted an error
-          return ParseError(rj::GetParseError_En(ok.Code()), " in row ", 
num_rows_);
+  Status Parse(const std::shared_ptr<Buffer>& json) override {
+    RETURN_NOT_OK(ReserveScalarStorage(json->size()));
+
+    const std::string_view input(reinterpret_cast<const char*>(json->data()),
+                                 json->size());
+    // TODO(GH-51809): Remove this check once Arrow requires simdjson 5.0.2,
+    // which fixes truncated_bytes() for empty input.
+    const int64_t input_size = input.size();
+    if (internal::ConsumeJsonWhitespace(input, /*trailing=*/false) == 
input_size) {
+      return Status::OK();
+    }
+
+    auto parse = [&](const auto& input) -> Status {
+      ARROW_ASSIGN_OR_RAISE(auto stream, 
arrow::internal::ResolveSimdjsonResult(
+                                             parser_.iterate_many(input),
+                                             "Failed to create JSON document 
stream"));
+
+      for (auto document_result : stream) {
+        ARROW_ASSIGN_OR_RAISE(
+            auto document,
+            arrow::internal::ResolveSimdjsonResult(
+                document_result, "Failed to iterate JSON document stream"));
+
+        if (num_rows_ == std::numeric_limits<int32_t>::max()) {
+          return Status::Invalid("Row count overflowed int32_t");
+        }
+
+        ARROW_ASSIGN_OR_RAISE(
+            auto value,
+            arrow::internal::ResolveSimdjsonResult(
+                document.get_value(), "JSON parse error: Failed to get JSON 
value"));
+
+        RETURN_NOT_OK(ParseValue(value));
+
+        ++num_rows_;
       }
+
+      if (stream.truncated_bytes() != 0) {
+        return ParseError("JSON document was truncated");
+      }
+
+      return Status::OK();
+    };
+
+    if (json->capacity() - json->size() >=
+        static_cast<int64_t>(simdjson::SIMDJSON_PADDING)) {
+      const auto padded_json = simdjson::padded_string_view(
+          reinterpret_cast<const char*>(json->data()), json->size(), 
json->capacity());
+      return parse(padded_json);
     }
-    return Status::Invalid("Row count overflowed int32_t");
+
+    // TODO(GH-51463): Investigate allocating input buffers with enough 
capacity
+    // for SIMDJSON padding to avoid copying in this case.
+    simdjson::padded_string padded_json(reinterpret_cast<const 
char*>(json->data()),
+                                        json->size());
+    return parse(padded_json);
   }
 
-  template <typename Handler>
-  Status DoParse(Handler& handler, const std::shared_ptr<Buffer>& json) {
-    RETURN_NOT_OK(ReserveScalarStorage(json->size()));
-    rj::MemoryStream ms(reinterpret_cast<const char*>(json->data()), 
json->size());
-    using InputStream = rj::EncodedInputStream<rj::UTF8<>, rj::MemoryStream>;
-    return DoParse(handler, InputStream(ms), 
static_cast<size_t>(json->size()));
+  template <Kind::type kind>
+  Status MaybePromoteFromNull() {
+    if (unexpected_field_behavior_ != UnexpectedFieldBehavior::InferType ||
+        builder_.kind != Kind::kNull) {
+      return Status::OK();
+    }
+
+    auto parent = builder_stack_.back();
+
+    if (parent.kind == Kind::kArray) {
+      auto list_builder = Cast<Kind::kArray>(parent);
+      DCHECK_EQ(list_builder->value_builder(), builder_);
+
+      RETURN_NOT_OK(builder_set_.MakeBuilder<kind>(builder_.index, &builder_));
+
+      list_builder = Cast<Kind::kArray>(parent);
+      list_builder->value_builder(builder_);
+    } else {
+      auto struct_builder = Cast<Kind::kObject>(parent);
+      DCHECK_EQ(struct_builder->field_builder(field_index_), builder_);
+
+      RETURN_NOT_OK(builder_set_.MakeBuilder<kind>(builder_.index, &builder_));
+
+      struct_builder = Cast<Kind::kObject>(parent);
+      struct_builder->field_builder(field_index_, builder_);
+    }
+
+    return Status::OK();
   }
 
-  /// \defgroup handlerbase-append-methods append non-nested values
-  ///
-  /// @{
+  Status ParseValue(sj::value value) {
+    ARROW_ASSIGN_OR_RAISE(auto type, arrow::internal::ResolveSimdjsonResult(
+                                         value.type(), "Failed to determine 
JSON type"));
 
-  template <Kind::type kind>
-  Status AppendScalar(BuilderPtr builder, std::string_view scalar) {
-    if (ARROW_PREDICT_FALSE(builder.kind != kind)) {
-      return IllegallyChangedTo(kind);
+    switch (type) {
+      case sj::json_type::null: {
+        ARROW_ASSIGN_OR_RAISE([[maybe_unused]] auto is_null,
+                              arrow::internal::ResolveSimdjsonResult(
+                                  value.is_null(), "Failed to validate JSON 
null"));
+        return Null();
+      }
+
+      case sj::json_type::boolean: {
+        RETURN_NOT_OK(MaybePromoteFromNull<Kind::kBoolean>());
+
+        ARROW_ASSIGN_OR_RAISE(auto boolean,
+                              arrow::internal::ResolveSimdjsonResult(
+                                  value.get_bool(), "Failed to get JSON 
boolean"));
+        return Bool(boolean);
+      }
+
+      case sj::json_type::string: {
+        RETURN_NOT_OK(MaybePromoteFromNull<Kind::kString>());
+
+        ARROW_ASSIGN_OR_RAISE(auto string,
+                              arrow::internal::ResolveSimdjsonResult(
+                                  value.get_string(), "Failed to get JSON 
string"));
+        return String(string);
+      }
+
+      case sj::json_type::number: {
+        RETURN_NOT_OK(MaybePromoteFromNull<Kind::kNumber>());
+        auto raw_number = value.raw_json_token();
+        RETURN_NOT_OK(arrow::internal::ResolveSimdjsonResult(
+            value.get_number(), "Failed to parse JSON number"));
+        raw_number.remove_suffix(
+            internal::ConsumeJsonWhitespace(raw_number, /*trailing=*/true));
+        return RawNumber(raw_number);
+      }
+
+      case sj::json_type::array:
+        RETURN_NOT_OK(MaybePromoteFromNull<Kind::kArray>());
+        return ParseArray(value);
+
+      case sj::json_type::object:
+        RETURN_NOT_OK(MaybePromoteFromNull<Kind::kObject>());
+        return ParseObject(value);
+
+      case sj::json_type::unknown:
+        return ParseError("Invalid JSON value");
     }
-    auto index = static_cast<int32_t>(scalar_values_builder_.length());
-    auto value_length = static_cast<int32_t>(scalar.size());
-    RETURN_NOT_OK(Cast<kind>(builder)->Append(index, value_length));
-    RETURN_NOT_OK(scalar_values_builder_.Reserve(1));
-    scalar_values_builder_.UnsafeAppend(scalar);
     return Status::OK();
   }
 
-  /// @}
+  Status ParseArray(sj::value value) {
+    constexpr auto kind = Kind::kArray;
+    if (ARROW_PREDICT_FALSE(builder_.kind != kind)) {
+      return IllegallyChangedTo(kind);
+    }
+
+    StartNested();
+
+    builder_ = Cast<kind>(builder_)->value_builder();
+
+    ARROW_ASSIGN_OR_RAISE(auto array, arrow::internal::ResolveSimdjsonResult(
+                                          value.get_array(), "Failed to get 
JSON array"));
+
+    int64_t size = 0;
+
+    for (auto element_result : array) {
+      ARROW_ASSIGN_OR_RAISE(auto element,
+                            arrow::internal::ResolveSimdjsonResult(
+                                element_result, "Failed to iterate JSON 
array"));
 
-  Status StartObjectImpl() {
+      RETURN_NOT_OK(ParseValue(element));
+      ++size;
+    }
+
+    EndNested();
+
+    auto list_builder = Cast<Kind::kArray>(builder_);
+    DCHECK_LE(size, std::numeric_limits<int32_t>::max());
+    return list_builder->Append(static_cast<int32_t>(size));
+  }
+
+  Status ParseObject(sj::value value) {
     constexpr auto kind = Kind::kObject;
     if (ARROW_PREDICT_FALSE(builder_.kind != kind)) {
       return IllegallyChangedTo(kind);
     }
+
     auto struct_builder = Cast<kind>(builder_);
     absent_fields_stack_.Push(struct_builder->num_fields(), true);
     StartNested();
-    return struct_builder->Append();
-  }
+    RETURN_NOT_OK(struct_builder->Append());
 
-  /// \brief helper for Key() functions
-  ///
-  /// sets the field builder with name key, or returns false if
-  /// there is no field with that name
-  bool SetFieldBuilder(std::string_view key, bool* duplicate_keys) {
-    auto parent = Cast<Kind::kObject>(builder_stack_.back());
-    field_index_ = parent->GetFieldIndex(key);
-    if (ARROW_PREDICT_FALSE(field_index_ == -1)) {
-      return false;
-    }
-    if (field_index_ < absent_fields_stack_.TopSize()) {
-      *duplicate_keys = !absent_fields_stack_[field_index_];
-    } else {
-      // When field_index is beyond the range of absent_fields_stack_ we have 
a duplicated
-      // field that wasn't declared in schema or previous records.
-      *duplicate_keys = true;
-    }
-    if (*duplicate_keys) {
-      status_ = ParseError("Column(", Path(), ") was specified twice in row ", 
num_rows_);
-      return false;
+    ARROW_ASSIGN_OR_RAISE(
+        auto object, arrow::internal::ResolveSimdjsonResult(value.get_object(),
+                                                            "Failed to get 
JSON object"));
+
+    for (auto field_result : object) {
+      ARROW_ASSIGN_OR_RAISE(
+          auto field,
+          arrow::internal::ResolveSimdjsonResult(
+              field_result, "JSON parse error: Failed to iterate JSON 
object"));
+
+      ARROW_ASSIGN_OR_RAISE(auto key,
+                            arrow::internal::ResolveSimdjsonResult(
+                                field.unescaped_key(), "Failed to get JSON 
object key"));
+
+      RETURN_NOT_OK(ParseObjectField(key, field.value()));
     }
-    builder_ = parent->field_builder(field_index_);
-    absent_fields_stack_[field_index_] = false;
-    return true;
-  }
 
-  Status EndObjectImpl() {
     auto parent = builder_stack_.back();
 
     auto expected_count = absent_fields_stack_.TopSize();
@@ -876,25 +964,42 @@ class HandlerBase : public BlockParser,
     return Status::OK();
   }
 
-  Status StartArrayImpl() {
-    constexpr auto kind = Kind::kArray;
-    if (ARROW_PREDICT_FALSE(builder_.kind != kind)) {
-      return IllegallyChangedTo(kind);
+  Status ParseObjectField(std::string_view key, sj::value value) {
+    auto parent = Cast<Kind::kObject>(builder_stack_.back());
+    field_index_ = parent->GetFieldIndex(key);
+    if (ARROW_PREDICT_FALSE(field_index_ == -1)) {
+      return HandleUnexpectedField(key, value);
     }
-    StartNested();
-    // append to the list builder in EndArrayImpl
-    builder_ = Cast<kind>(builder_)->value_builder();
-    return Status::OK();
+    bool duplicate_keys;
+    if (field_index_ < absent_fields_stack_.TopSize()) {
+      duplicate_keys = !absent_fields_stack_[field_index_];
+    } else {
+      // When field_index is beyond the range of absent_fields_stack_ we have 
a duplicated
+      // field that wasn't declared in schema or previous records.
+      duplicate_keys = true;
+    }
+    if (duplicate_keys) {
+      return ParseError("Column(", Path(), ") was specified twice in row ", 
num_rows_);
+    }
+    builder_ = parent->field_builder(field_index_);
+    absent_fields_stack_[field_index_] = false;
+    return ParseValue(value);
   }
 
-  Status EndArrayImpl(rj::SizeType size) {
-    EndNested();
-    // append to list_builder here
-    auto list_builder = Cast<Kind::kArray>(builder_);
-    return list_builder->Append(size);
+  template <Kind::type kind>
+  Status AppendScalar(BuilderPtr builder, std::string_view scalar) {
+    if (ARROW_PREDICT_FALSE(builder.kind != kind)) {
+      return IllegallyChangedTo(kind);
+    }
+    auto index = static_cast<int32_t>(scalar_values_builder_.length());
+    auto value_length = static_cast<int32_t>(scalar.size());
+    RETURN_NOT_OK(Cast<kind>(builder)->Append(index, value_length));
+    RETURN_NOT_OK(scalar_values_builder_.Reserve(1));
+    scalar_values_builder_.UnsafeAppend(scalar);
+    return Status::OK();
   }
 
-  /// helper method for StartArray and StartObject
+  /// helper method for ParseArray and ParseObject
   /// adds the current builder to a stack so its
   /// children can be visited and parsed.
   void StartNested() {
@@ -928,7 +1033,7 @@ class HandlerBase : public BlockParser,
     return scalar_values_builder_.ReserveData(size - available_storage);
   }
 
-  Status status_;
+  UnexpectedFieldBehavior unexpected_field_behavior_;
   RawBuilderSet builder_set_;
   BuilderPtr builder_;
   // top of this stack is the parent of builder_
@@ -941,232 +1046,7 @@ class HandlerBase : public BlockParser,
   // top of this stack == field_index_
   std::vector<int> field_index_stack_;
   StringBuilder scalar_values_builder_;
-};
-
-template <UnexpectedFieldBehavior>
-class Handler;
-
-template <>
-class Handler<UnexpectedFieldBehavior::Error> : public HandlerBase {
- public:
-  using HandlerBase::HandlerBase;
-
-  Status Parse(const std::shared_ptr<Buffer>& json) override {
-    return DoParse(*this, json);
-  }
-
-  /// \ingroup rapidjson-handler-interface
-  ///
-  /// if an unexpected field is encountered, emit a parse error and bail
-  bool Key(const char* key, rj::SizeType len, ...) {
-    bool duplicate_keys = false;
-    if (ARROW_PREDICT_FALSE(
-            SetFieldBuilder(std::string_view(key, len), &duplicate_keys))) {
-      return true;
-    }
-    if (!duplicate_keys) {
-      status_ = ParseError("unexpected field");
-    }
-    return false;
-  }
-};
-
-template <>
-class Handler<UnexpectedFieldBehavior::Ignore> : public HandlerBase {
- public:
-  using HandlerBase::HandlerBase;
-
-  Status Parse(const std::shared_ptr<Buffer>& json) override {
-    return DoParse(*this, json);
-  }
-
-  bool Null() {
-    if (Skipping()) {
-      return true;
-    }
-    return HandlerBase::Null();
-  }
-
-  bool Bool(bool value) {
-    if (Skipping()) {
-      return true;
-    }
-    return HandlerBase::Bool(value);
-  }
-
-  bool RawNumber(const char* data, rj::SizeType size, ...) {
-    if (Skipping()) {
-      return true;
-    }
-    return HandlerBase::RawNumber(data, size);
-  }
-
-  bool String(const char* data, rj::SizeType size, ...) {
-    if (Skipping()) {
-      return true;
-    }
-    return HandlerBase::String(data, size);
-  }
-
-  bool StartObject() {
-    ++depth_;
-    if (Skipping()) {
-      return true;
-    }
-    return HandlerBase::StartObject();
-  }
-
-  /// \ingroup rapidjson-handler-interface
-  ///
-  /// if an unexpected field is encountered, skip until its value has been 
consumed
-  bool Key(const char* key, rj::SizeType len, ...) {
-    MaybeStopSkipping();
-    if (Skipping()) {
-      return true;
-    }
-    bool duplicate_keys = false;
-    if (ARROW_PREDICT_TRUE(
-            SetFieldBuilder(std::string_view(key, len), &duplicate_keys))) {
-      return true;
-    }
-    if (ARROW_PREDICT_FALSE(duplicate_keys)) {
-      return false;
-    }
-    skip_depth_ = depth_;
-    return true;
-  }
-
-  bool EndObject(...) {
-    MaybeStopSkipping();
-    --depth_;
-    if (Skipping()) {
-      return true;
-    }
-    return HandlerBase::EndObject();
-  }
-
-  bool StartArray() {
-    if (Skipping()) {
-      return true;
-    }
-    return HandlerBase::StartArray();
-  }
-
-  bool EndArray(rj::SizeType size) {
-    if (Skipping()) {
-      return true;
-    }
-    return HandlerBase::EndArray(size);
-  }
-
- private:
-  bool Skipping() { return depth_ >= skip_depth_; }
-
-  void MaybeStopSkipping() {
-    if (skip_depth_ == depth_) {
-      skip_depth_ = std::numeric_limits<int>::max();
-    }
-  }
-
-  int depth_ = 0;
-  int skip_depth_ = std::numeric_limits<int>::max();
-};
-
-template <>
-class Handler<UnexpectedFieldBehavior::InferType> : public HandlerBase {
- public:
-  using HandlerBase::HandlerBase;
-
-  Status Parse(const std::shared_ptr<Buffer>& json) override {
-    return DoParse(*this, json);
-  }
-
-  bool Bool(bool value) {
-    if (ARROW_PREDICT_FALSE(MaybePromoteFromNull<Kind::kBoolean>())) {
-      return false;
-    }
-    return HandlerBase::Bool(value);
-  }
-
-  bool RawNumber(const char* data, rj::SizeType size, ...) {
-    if (ARROW_PREDICT_FALSE(MaybePromoteFromNull<Kind::kNumber>())) {
-      return false;
-    }
-    return HandlerBase::RawNumber(data, size);
-  }
-
-  bool String(const char* data, rj::SizeType size, ...) {
-    if (ARROW_PREDICT_FALSE(MaybePromoteFromNull<Kind::kString>())) {
-      return false;
-    }
-    return HandlerBase::String(data, size);
-  }
-
-  bool StartObject() {
-    if (ARROW_PREDICT_FALSE(MaybePromoteFromNull<Kind::kObject>())) {
-      return false;
-    }
-    return HandlerBase::StartObject();
-  }
-
-  /// \ingroup rapidjson-handler-interface
-  ///
-  /// If an unexpected field is encountered, add a new builder to
-  /// the current parent builder. It is added as a NullBuilder with
-  /// (parent.length - 1) leading nulls. The next value parsed
-  /// will probably trigger promotion of this field from null
-  bool Key(const char* key, rj::SizeType len, ...) {
-    bool duplicate_keys = false;
-    if (ARROW_PREDICT_TRUE(
-            SetFieldBuilder(std::string_view(key, len), &duplicate_keys))) {
-      return true;
-    }
-    if (ARROW_PREDICT_FALSE(duplicate_keys)) {
-      return false;
-    }
-    auto struct_builder = Cast<Kind::kObject>(builder_stack_.back());
-    auto leading_nulls = static_cast<uint32_t>(struct_builder->length() - 1);
-    builder_ = BuilderPtr(Kind::kNull, leading_nulls, true);
-    field_index_ = struct_builder->AddField(std::string_view(key, len), 
builder_);
-    return true;
-  }
-
-  bool StartArray() {
-    if (ARROW_PREDICT_FALSE(MaybePromoteFromNull<Kind::kArray>())) {
-      return false;
-    }
-    return HandlerBase::StartArray();
-  }
-
- private:
-  // return true if a terminal error was encountered
-  template <Kind::type kind>
-  bool MaybePromoteFromNull() {
-    if (ARROW_PREDICT_TRUE(builder_.kind != Kind::kNull)) {
-      return false;
-    }
-    auto parent = builder_stack_.back();
-    if (parent.kind == Kind::kArray) {
-      auto list_builder = Cast<Kind::kArray>(parent);
-      DCHECK_EQ(list_builder->value_builder(), builder_);
-      status_ = builder_set_.MakeBuilder<kind>(builder_.index, &builder_);
-      if (ARROW_PREDICT_FALSE(!status_.ok())) {
-        return true;
-      }
-      list_builder = Cast<Kind::kArray>(parent);
-      list_builder->value_builder(builder_);
-    } else {
-      auto struct_builder = Cast<Kind::kObject>(parent);
-      DCHECK_EQ(struct_builder->field_builder(field_index_), builder_);
-      status_ = builder_set_.MakeBuilder<kind>(builder_.index, &builder_);
-      if (ARROW_PREDICT_FALSE(!status_.ok())) {
-        return true;
-      }
-      struct_builder = Cast<Kind::kObject>(parent);
-      struct_builder->field_builder(field_index_, builder_);
-    }
-    return false;
-  }
+  sj::parser parser_;
 };
 
 Status BlockParser::Make(MemoryPool* pool, const ParseOptions& options,
@@ -1174,20 +1054,10 @@ Status BlockParser::Make(MemoryPool* pool, const 
ParseOptions& options,
   DCHECK(options.unexpected_field_behavior == 
UnexpectedFieldBehavior::InferType ||
          options.explicit_schema != nullptr);
 
-  switch (options.unexpected_field_behavior) {
-    case UnexpectedFieldBehavior::Ignore: {
-      *out = std::make_unique<Handler<UnexpectedFieldBehavior::Ignore>>(pool);
-      break;
-    }
-    case UnexpectedFieldBehavior::Error: {
-      *out = std::make_unique<Handler<UnexpectedFieldBehavior::Error>>(pool);
-      break;
-    }
-    case UnexpectedFieldBehavior::InferType:
-      *out = 
std::make_unique<Handler<UnexpectedFieldBehavior::InferType>>(pool);
-      break;
-  }
-  return static_cast<HandlerBase&>(**out).Initialize(options.explicit_schema);
+  auto parser = std::make_unique<ParseImpl>(pool, 
options.unexpected_field_behavior);
+  RETURN_NOT_OK(parser->Initialize(options.explicit_schema));
+  *out = std::move(parser);
+  return Status::OK();
 }
 
 Status BlockParser::Make(const ParseOptions& options, 
std::unique_ptr<BlockParser>* out) {
diff --git a/cpp/src/arrow/json/parser_test.cc 
b/cpp/src/arrow/json/parser_test.cc
index 1b107aa020f..28c54b3c5cd 100644
--- a/cpp/src/arrow/json/parser_test.cc
+++ b/cpp/src/arrow/json/parser_test.cc
@@ -235,6 +235,24 @@ TEST(BlockParserWithSchema, FailOnIncompleteJson) {
   ASSERT_RAISES(Invalid, ParseFromString(options, "{\"a\":0, \"b\"", &parsed));
 }
 
+TEST(BlockParserWithSchema, ValidateIgnoredFields) {
+  auto options = ParseOptions::Defaults();
+  options.explicit_schema = schema({field("known", int64())});
+  options.unexpected_field_behavior = UnexpectedFieldBehavior::Ignore;
+
+  std::shared_ptr<Array> parsed;
+  // Ignored fields should still be validated for malformed JSON.
+  Status error = ParseFromString(options, R"({"known": 1, "ignored": [1,]})", 
&parsed);
+  ASSERT_RAISES(Invalid, error);
+  EXPECT_THAT(error.message(), testing::StartsWith("Invalid JSON value"));
+}
+
+TEST(BlockParserWithSchema, NumberWithWhitespace) {
+  auto options = ParseOptions::Defaults();
+  options.explicit_schema = schema({field("a", int64())});
+  AssertParseColumns(options, R"({"a":   123   })", {field("a", utf8())}, 
{R"(["123"])"});
+}
+
 TEST(BlockParser, Basics) {
   auto options = ParseOptions::Defaults();
   options.unexpected_field_behavior = UnexpectedFieldBehavior::InferType;
@@ -305,7 +323,7 @@ TEST(BlockParser, FailOnInvalidEOF) {
   auto status = ParseFromString(ParseOptions::Defaults(), "}", &parsed);
   ASSERT_RAISES(Invalid, status);
   EXPECT_THAT(status.message(),
-              ::testing::StartsWith("JSON parse error: The document is 
empty"));
+              ::testing::StartsWith("JSON parse error: JSON document was 
truncated"));
 }
 
 TEST(BlockParser, AdHoc) {
@@ -321,5 +339,21 @@ TEST(BlockParser, AdHoc) {
        R"([{"c":true, "d": "1991-02-03"}, {"c":false, "d":"2019-04-01"}])"});
 }
 
+TEST(BlockParser, TopLevelNull) {
+  std::shared_ptr<Array> parsed;
+  ASSERT_RAISES(Invalid, ParseFromString(ParseOptions::Defaults(), "null\n", 
&parsed));
+  ASSERT_RAISES(Invalid,
+                ParseFromString(ParseOptions::Defaults(), "{\"a\":1}\nnull\n", 
&parsed));
+}
+
+TEST(BlockParser, NullTypeRejectsNonNullUnderError) {
+  auto options = ParseOptions::Defaults();
+  options.explicit_schema = schema({field("a", null())});
+  options.unexpected_field_behavior = UnexpectedFieldBehavior::Error;
+  std::shared_ptr<Array> parsed;
+  // Should reject — "a" is typed null(), but 5 is not null.
+  ASSERT_RAISES(Invalid, ParseFromString(options, R"({"a": 5})", &parsed));
+}
+
 }  // namespace json
 }  // namespace arrow
diff --git a/cpp/src/arrow/json/reader_test.cc 
b/cpp/src/arrow/json/reader_test.cc
index 549a4948057..bc533595614 100644
--- a/cpp/src/arrow/json/reader_test.cc
+++ b/cpp/src/arrow/json/reader_test.cc
@@ -696,10 +696,10 @@ TEST_P(StreamingReaderTest, PropagateParsingErrors) {
 
   read_options_.block_size = 16;
   EXPECT_RAISES_WITH_MESSAGE_THAT(
-      Invalid, ::testing::StartsWith("Invalid: JSON parse error: Invalid 
value"),
+      Invalid, ::testing::StartsWith("Invalid: JSON parse error: Invalid JSON 
value"),
       MakeReader(bad_first_block));
   EXPECT_RAISES_WITH_MESSAGE_THAT(
-      Invalid, ::testing::StartsWith("Invalid: JSON parse error: Invalid 
value"),
+      Invalid, ::testing::StartsWith("Invalid: JSON parse error: Invalid JSON 
value"),
       MakeReader(bad_first_block_after_empty));
 
   std::shared_ptr<RecordBatch> batch;
@@ -711,11 +711,9 @@ TEST_P(StreamingReaderTest, PropagateParsingErrors) {
   EXPECT_EQ(reader->bytes_processed(), 13);
   ASSERT_BATCHES_EQUAL(*RecordBatchFromJSON(test_schema, R"([{"n":10000}])"), 
*batch);
 
-  EXPECT_RAISES_WITH_MESSAGE_THAT(
-      Invalid,
-      ::testing::StartsWith(
-          "Invalid: JSON parse error: Missing a comma or '}' after an object 
member"),
-      reader->ReadNext(&batch));
+  EXPECT_RAISES_WITH_MESSAGE_THAT(Invalid,
+                                  ::testing::StartsWith("Invalid: JSON parse 
error"),
+                                  reader->ReadNext(&batch));
   EXPECT_EQ(reader->bytes_processed(), 13);
   AssertReadEnd(reader);
   EXPECT_EQ(reader->bytes_processed(), 13);
@@ -1025,5 +1023,26 @@ TEST_F(AsyncStreamingReaderTest, 
StressSharedIoAndCpuExecutor) {
   AssertBatchSequenceEquals(expected.batches, batches);
 }
 
+TEST(ReaderTest, FailOnMalformedNumbers) {
+  auto read_options = ReadOptions::Defaults();
+  auto parse_options = ParseOptions::Defaults();
+
+  const std::vector<std::string> malformed = {
+      R"({"a": 01})",
+      R"({"a": 1.})",
+  };
+
+  // Malformed numbers should be rejected
+  for (const bool use_threads : {false, true}) {
+    read_options.use_threads = use_threads;
+
+    for (const auto& json : malformed) {
+      EXPECT_RAISES_WITH_MESSAGE_THAT(
+          Invalid, ::testing::StartsWith("Invalid: Failed to parse JSON 
number"),
+          ReadToTable(json, read_options, parse_options));
+    }
+  }
+}
+
 }  // namespace json
 }  // namespace arrow
diff --git a/cpp/src/arrow/util/simdjson_internal.cc 
b/cpp/src/arrow/util/simdjson_internal.cc
index 146b48d9eb7..d845eab0d47 100644
--- a/cpp/src/arrow/util/simdjson_internal.cc
+++ b/cpp/src/arrow/util/simdjson_internal.cc
@@ -581,4 +581,18 @@ Status ValidateJsonDocument(simdjson::ondemand::parser& 
parser,
   return ConsumeJsonValue(value);
 }
 
+/// Returns the position of the first non-whitespace character when trailing 
is false,
+/// or the number of trailing whitespace characters when trailing is true.
+// XXX We could try to SIMD-accelerate this routine.
+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);
+  }
+
+  const auto pos = view.find_last_not_of(" \t\r\n");
+  return static_cast<int64_t>(pos == std::string_view::npos ? view.size()
+                                                            : view.size() - 
pos - 1);
+}
+
 }  // namespace arrow::internal
diff --git a/cpp/src/arrow/util/simdjson_internal.h 
b/cpp/src/arrow/util/simdjson_internal.h
index 5c52f7648c6..d5816522bee 100644
--- a/cpp/src/arrow/util/simdjson_internal.h
+++ b/cpp/src/arrow/util/simdjson_internal.h
@@ -266,7 +266,7 @@ Status VisitJsonValue(simdjson::ondemand::value value, 
ObjectFn&& object_fn,
     }
 
     case simdjson::ondemand::json_type::unknown:
-      return Status::Invalid("Unknown JSON type");
+      return Status::Invalid("Invalid JSON value");
   }
 
   return Status::Invalid("Unreachable");
@@ -350,4 +350,6 @@ ARROW_EXPORT Status 
ConsumeJsonValue(simdjson::ondemand::value value);
 ARROW_EXPORT Status ValidateJsonDocument(simdjson::ondemand::parser& parser,
                                          simdjson::padded_string& json);
 
+ARROW_EXPORT int64_t ConsumeJsonWhitespace(std::string_view view, bool 
trailing);
+
 }  // namespace arrow::internal
diff --git a/python/pyarrow/tests/test_json.py 
b/python/pyarrow/tests/test_json.py
index ac2a027cfa2..e7ab574ead4 100644
--- a/python/pyarrow/tests/test_json.py
+++ b/python/pyarrow/tests/test_json.py
@@ -452,7 +452,7 @@ class BaseTestStreamingJSONRead(BaseTestJSON):
         read_options = ReadOptions()
         read_options.block_size = 16
         with pytest.raises(pa.ArrowInvalid,
-                           match="JSON parse error: Invalid value.*"):
+                           match="JSON parse error: Invalid JSON value.*"):
             self.open_bytes(bad_first_block, read_options=read_options)
 
     def test_bad_middle_parse_after_empty(self):
@@ -460,7 +460,7 @@ class BaseTestStreamingJSONRead(BaseTestJSON):
         read_options = ReadOptions()
         read_options.block_size = 16
         with pytest.raises(pa.ArrowInvalid,
-                           match="JSON parse error: Invalid value.*"):
+                           match="JSON parse error: Invalid JSON value.*"):
             self.open_bytes(bad_first_block, read_options=read_options)
 
     def test_bad_middle_parse(self):
@@ -476,8 +476,7 @@ class BaseTestStreamingJSONRead(BaseTestJSON):
         }
         with pytest.raises(
             pa.ArrowInvalid,
-            match="JSON parse error:\
- Missing a comma or '}' after an object member*"
+            match="JSON parse error:"
         ):
             reader.read_next_batch()
 

Reply via email to