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 6c2c8c7514 GH-50567: [C++] Introduce JsonWriter and migrate 
integration JSON writer (#50568)
6c2c8c7514 is described below

commit 6c2c8c751430026625d12627d8c4f56e6ab80355
Author: Aaditya Srinivasan <[email protected]>
AuthorDate: Tue Jul 28 20:57:36 2026 +0530

    GH-50567: [C++] Introduce JsonWriter and migrate integration JSON writer 
(#50568)
    
    ### What changes are included?
    
    This PR introduces a reusable `JsonWriter` wrapper around simdjson's JSON 
builder API and migrates the integration JSON writer to use it instead of 
RapidJSON.
    
    Specifically, this PR:
    
    - Adds a reusable `JsonWriter` abstraction in `arrow/json`.
    - Migrates the integration JSON writer implementation to `JsonWriter`.
    - Updates the integration tests to use `JsonWriter`.
    - Adds unit tests for `JsonWriter`.
    
    This is part of the incremental migration from RapidJSON to simdjson.
    
    ### Performance
    
    I compared the serialization performance of 
`IntegrationJsonWriter::WriteRecordBatch()` + `Finish()` using a temporary 
benchmark with a 1M-row `RecordBatch`. The benchmark was run 10 times on both 
`main` and this branch.
    
    | Branch | Average Time |
    |--------|-------------:|
    | `main` | 2157.4 ms |
    | This PR | 2153.6 ms |
    
    No measurable performance regression was observed.
    
    ### Are these changes tested?
    
    Yes.
    
    I added unit tests for `JsonWriter` and verified that both JSON and 
integration tests pass locally:
    
    - `arrow-json-test`
    - `arrow-json-integration-test`
    
    ### Are there any user-facing changes?
    
    No.
    * GitHub Issue: #50567
    
    Authored-by: Aaditya Srinivasan <[email protected]>
    Signed-off-by: Antoine Pitrou <[email protected]>
---
 ci/docker/ubuntu-24.04-cpp.dockerfile              |   1 +
 cpp/cmake_modules/ThirdpartyToolchain.cmake        |   2 +-
 cpp/src/arrow/CMakeLists.txt                       |   4 +-
 cpp/src/arrow/integration/CMakeLists.txt           |   7 +-
 cpp/src/arrow/integration/json_integration.cc      |  35 +++--
 cpp/src/arrow/integration/json_integration_test.cc |  12 +-
 cpp/src/arrow/integration/json_internal.cc         |  45 +++---
 cpp/src/arrow/integration/json_internal.h          |  17 ++-
 cpp/src/arrow/integration/meson.build              |   7 +-
 cpp/src/arrow/json/CMakeLists.txt                  |   6 +-
 cpp/src/arrow/json/json_writer_internal.cc         | 118 ++++++++++++++++
 cpp/src/arrow/json/json_writer_internal.h          |  66 +++++++++
 cpp/src/arrow/json/json_writer_internal_test.cc    | 152 +++++++++++++++++++++
 cpp/src/arrow/json/meson.build                     |   1 +
 cpp/src/arrow/json/reader_test.cc                  |  12 +-
 cpp/src/arrow/meson.build                          |   3 +-
 dev/tasks/tasks.yml                                |   2 +-
 17 files changed, 421 insertions(+), 69 deletions(-)

diff --git a/ci/docker/ubuntu-24.04-cpp.dockerfile 
b/ci/docker/ubuntu-24.04-cpp.dockerfile
index ce2a30d980..00cc8796c5 100644
--- a/ci/docker/ubuntu-24.04-cpp.dockerfile
+++ b/ci/docker/ubuntu-24.04-cpp.dockerfile
@@ -221,4 +221,5 @@ ENV absl_SOURCE=BUNDLED \
     PARQUET_BUILD_EXECUTABLES=ON \
     PATH=/usr/lib/ccache/:$PATH \
     PYTHON=python3 \
+    simdjson_SOURCE=BUNDLED \
     xsimd_SOURCE=BUNDLED
diff --git a/cpp/cmake_modules/ThirdpartyToolchain.cmake 
b/cpp/cmake_modules/ThirdpartyToolchain.cmake
index 4bef30f7df..9fc1e3d0aa 100644
--- a/cpp/cmake_modules/ThirdpartyToolchain.cmake
+++ b/cpp/cmake_modules/ThirdpartyToolchain.cmake
@@ -2843,7 +2843,7 @@ function(build_simdjson)
 endfunction()
 
 if(ARROW_WITH_SIMDJSON)
-  set(ARROW_SIMDJSON_REQUIRED_VERSION "3.0.0")
+  set(ARROW_SIMDJSON_REQUIRED_VERSION "4.0.0")
   resolve_dependency(simdjson
                      FORCE_ANY_NEWER_VERSION
                      TRUE
diff --git a/cpp/src/arrow/CMakeLists.txt b/cpp/src/arrow/CMakeLists.txt
index 0b21fec605..64fb4d12a5 100644
--- a/cpp/src/arrow/CMakeLists.txt
+++ b/cpp/src/arrow/CMakeLists.txt
@@ -733,7 +733,8 @@ if(ARROW_BUILD_INTEGRATION OR ARROW_BUILD_TESTS)
   arrow_add_object_library(ARROW_INTEGRATION integration/json_integration.cc
                            integration/json_internal.cc)
   foreach(ARROW_INTEGRATION_TARGET ${ARROW_INTEGRATION_TARGETS})
-    target_link_libraries(${ARROW_INTEGRATION_TARGET} PRIVATE RapidJSON)
+    target_link_libraries(${ARROW_INTEGRATION_TARGET} PRIVATE RapidJSON
+                                                              
simdjson::simdjson)
   endforeach()
 else()
   set(ARROW_INTEGRATION_TARGET_SHARED)
@@ -1039,6 +1040,7 @@ if(ARROW_JSON)
                            json/chunker.cc
                            json/converter.cc
                            json/from_string.cc
+                           json/json_writer_internal.cc
                            json/object_parser.cc
                            json/object_writer.cc
                            json/parser.cc
diff --git a/cpp/src/arrow/integration/CMakeLists.txt 
b/cpp/src/arrow/integration/CMakeLists.txt
index 267d0adf11..8374d6de10 100644
--- a/cpp/src/arrow/integration/CMakeLists.txt
+++ b/cpp/src/arrow/integration/CMakeLists.txt
@@ -21,12 +21,17 @@ arrow_install_all_headers("arrow/integration")
 # - an executable that can be called to answer integration test requests
 # - a self-(unit)test for the C++ side of integration testing
 if(ARROW_BUILD_TESTS)
-  add_arrow_test(json_integration_test EXTRA_LINK_LIBS RapidJSON 
${GFLAGS_LIBRARIES})
+  add_arrow_test(json_integration_test
+                 EXTRA_LINK_LIBS
+                 RapidJSON
+                 simdjson::simdjson
+                 ${GFLAGS_LIBRARIES})
   add_dependencies(arrow-integration arrow-json-integration-test)
 elseif(ARROW_BUILD_INTEGRATION)
   add_executable(arrow-json-integration-test json_integration_test.cc)
   target_link_libraries(arrow-json-integration-test
                         RapidJSON
+                        simdjson::simdjson
                         ${ARROW_TEST_LINK_LIBS}
                         ${GFLAGS_LIBRARIES}
                         ${ARROW_GTEST_GTEST})
diff --git a/cpp/src/arrow/integration/json_integration.cc 
b/cpp/src/arrow/integration/json_integration.cc
index f1844bcbda..d7c0fa59e4 100644
--- a/cpp/src/arrow/integration/json_integration.cc
+++ b/cpp/src/arrow/integration/json_integration.cc
@@ -27,6 +27,7 @@
 #include "arrow/integration/json_internal.h"
 #include "arrow/io/file.h"
 #include "arrow/ipc/dictionary.h"
+#include "arrow/json/json_writer_internal.h"
 #include "arrow/record_batch.h"
 #include "arrow/result.h"
 #include "arrow/status.h"
@@ -36,6 +37,8 @@
 using arrow::ipc::DictionaryFieldMapper;
 using arrow::ipc::DictionaryMemo;
 
+using JsonWriter = arrow::json::JsonWriter;
+
 namespace arrow::internal::integration {
 
 // ----------------------------------------------------------------------
@@ -44,13 +47,10 @@ namespace arrow::internal::integration {
 class IntegrationJsonWriter::Impl {
  public:
   explicit Impl(const std::shared_ptr<Schema>& schema)
-      : schema_(schema), mapper_(*schema), first_batch_written_(false) {
-    writer_.reset(new RjWriter(string_buffer_));
-  }
-
+      : schema_(schema), mapper_(*schema), first_batch_written_(false) {}
   Status Start() {
-    writer_->StartObject();
-    RETURN_NOT_OK(json::WriteSchema(*schema_, mapper_, writer_.get()));
+    writer_.StartObject();
+    RETURN_NOT_OK(json::WriteSchema(*schema_, mapper_, &writer_));
     return Status::OK();
   }
 
@@ -59,26 +59,26 @@ class IntegrationJsonWriter::Impl {
 
     // Write dictionaries, if any
     if (!dictionaries.empty()) {
-      writer_->Key("dictionaries");
-      writer_->StartArray();
+      writer_.Key("dictionaries");
+      writer_.StartArray();
       for (const auto& entry : dictionaries) {
-        RETURN_NOT_OK(json::WriteDictionary(entry.first, entry.second, 
writer_.get()));
+        RETURN_NOT_OK(json::WriteDictionary(entry.first, entry.second, 
&writer_));
       }
-      writer_->EndArray();
+      writer_.EndArray();
     }
 
     // Record batches
-    writer_->Key("batches");
-    writer_->StartArray();
+    writer_.Key("batches");
+    writer_.StartArray();
     first_batch_written_ = true;
     return Status::OK();
   }
 
   Result<std::string> Finish() {
-    writer_->EndArray();  // Record batches
-    writer_->EndObject();
+    writer_.EndArray();  // Record batches
+    writer_.EndObject();
 
-    return string_buffer_.GetString();
+    return std::string(writer_.GetString());
   }
 
   Status WriteRecordBatch(const RecordBatch& batch) {
@@ -87,7 +87,7 @@ class IntegrationJsonWriter::Impl {
     if (!first_batch_written_) {
       RETURN_NOT_OK(FirstRecordBatch(batch));
     }
-    return json::WriteRecordBatch(batch, writer_.get());
+    return json::WriteRecordBatch(batch, &writer_);
   }
 
  private:
@@ -96,8 +96,7 @@ class IntegrationJsonWriter::Impl {
 
   bool first_batch_written_;
 
-  rj::StringBuffer string_buffer_;
-  std::unique_ptr<RjWriter> writer_;
+  JsonWriter writer_;
 };
 
 IntegrationJsonWriter::IntegrationJsonWriter(const std::shared_ptr<Schema>& 
schema) {
diff --git a/cpp/src/arrow/integration/json_integration_test.cc 
b/cpp/src/arrow/integration/json_integration_test.cc
index 23105e8ca1..e7a55d13e0 100644
--- a/cpp/src/arrow/integration/json_integration_test.cc
+++ b/cpp/src/arrow/integration/json_integration_test.cc
@@ -38,6 +38,7 @@
 #include "arrow/ipc/reader.h"
 #include "arrow/ipc/test_common.h"
 #include "arrow/ipc/writer.h"
+#include "arrow/json/json_writer_internal.h"
 #include "arrow/pretty_print.h"
 #include "arrow/status.h"
 #include "arrow/testing/builder.h"
@@ -723,8 +724,7 @@ static const char* json_example6 = R"example(
 )example";
 
 void TestSchemaRoundTrip(const std::shared_ptr<Schema>& schema) {
-  rj::StringBuffer sb;
-  rj::Writer<rj::StringBuffer> writer(sb);
+  arrow::json::JsonWriter writer;
 
   DictionaryFieldMapper mapper(*schema);
 
@@ -732,7 +732,7 @@ void TestSchemaRoundTrip(const std::shared_ptr<Schema>& 
schema) {
   ASSERT_OK(json::WriteSchema(*schema, mapper, &writer));
   writer.EndObject();
 
-  std::string json_schema = sb.GetString();
+  std::string json_schema(writer.GetString());
 
   rj::Document d;
   // Pass explicit size to avoid ASAN issues with
@@ -748,12 +748,11 @@ void TestSchemaRoundTrip(const std::shared_ptr<Schema>& 
schema) {
 void TestArrayRoundTrip(const Array& array) {
   static std::string name = "dummy";
 
-  rj::StringBuffer sb;
-  rj::Writer<rj::StringBuffer> writer(sb);
+  arrow::json::JsonWriter writer;
 
   ASSERT_OK(json::WriteArray(name, array, &writer));
 
-  std::string array_as_json = sb.GetString();
+  std::string array_as_json(writer.GetString());
 
   rj::Document d;
   // Pass explicit size to avoid ASAN issues with
@@ -768,7 +767,6 @@ void TestArrayRoundTrip(const Array& array) {
       json::ReadArray(default_memory_pool(), d, ::arrow::field(name, 
array.type())));
   ASSERT_OK(result_array->ValidateFull());
 
-  // std::cout << array_as_json << std::endl;
   CompareArraysDetailed(0, *result_array, array);
 }
 
diff --git a/cpp/src/arrow/integration/json_internal.cc 
b/cpp/src/arrow/integration/json_internal.cc
index 1e57be51b0..d0baad67e1 100644
--- a/cpp/src/arrow/integration/json_internal.cc
+++ b/cpp/src/arrow/integration/json_internal.cc
@@ -36,6 +36,7 @@
 #include "arrow/array/builder_time.h"
 #include "arrow/extension_type.h"
 #include "arrow/ipc/dictionary.h"
+#include "arrow/json/json_writer_internal.h"
 #include "arrow/record_batch.h"
 #include "arrow/result.h"
 #include "arrow/scalar.h"
@@ -64,6 +65,8 @@ using arrow::ipc::DictionaryFieldMapper;
 using arrow::ipc::DictionaryMemo;
 using arrow::ipc::internal::FieldPosition;
 
+using JsonWriter = arrow::json::JsonWriter;
+
 namespace arrow::internal::integration::json {
 
 namespace {
@@ -118,7 +121,7 @@ Result<std::string_view> GetStringView(const rj::Value& 
str) {
 class SchemaWriter {
  public:
   explicit SchemaWriter(const Schema& schema, const DictionaryFieldMapper& 
mapper,
-                        RjWriter* writer)
+                        JsonWriter* writer)
       : schema_(schema), mapper_(mapper), writer_(writer) {}
 
   Status Write() {
@@ -460,7 +463,7 @@ class SchemaWriter {
  private:
   const Schema& schema_;
   const DictionaryFieldMapper& mapper_;
-  RjWriter* writer_;
+  JsonWriter* writer_;
 };
 
 Status SchemaWriter::VisitType(const DataType& type) {
@@ -469,7 +472,7 @@ Status SchemaWriter::VisitType(const DataType& type) {
 
 class ArrayWriter {
  public:
-  ArrayWriter(const std::string& name, const Array& array, RjWriter* writer)
+  ArrayWriter(const std::string& name, const Array& array, JsonWriter* writer)
       : name_(name), array_(array), writer_(writer) {}
 
   Status Write() { return VisitArray(name_, array_); }
@@ -490,11 +493,7 @@ class ArrayWriter {
     return Status::OK();
   }
 
-  void WriteRawNumber(std::string_view v) {
-    // Avoid RawNumber() as it misleadingly adds quotes
-    // (see https://github.com/Tencent/rapidjson/pull/1155)
-    writer_->RawValue(v.data(), v.size(), rj::kNumberType);
-  }
+  void WriteRawNumber(std::string_view v) { writer_->RawValue(v); }
 
   template <typename ArrayType, typename TypeClass = typename 
ArrayType::TypeClass,
             typename CType = typename TypeClass::c_type>
@@ -522,11 +521,10 @@ class ArrayWriter {
     for (int64_t i = 0; i < arr.length(); ++i) {
       if (arr.IsValid(i)) {
         fmt(arr.Value(i), [&](std::string_view repr) {
-          writer_->String(repr.data(), static_cast<rj::SizeType>(repr.size()));
+          writer_->String(std::string_view(repr.data(), repr.size()));
         });
       } else {
-        writer_->String(null_string.data(),
-                        static_cast<rj::SizeType>(null_string.size()));
+        writer_->String(std::string_view(null_string.data(), 
null_string.size()));
       }
     }
   }
@@ -553,7 +551,7 @@ class ArrayWriter {
       if constexpr (Type::is_utf8) {
         // UTF8 string, write as is
         auto view = arr.GetView(i);
-        writer_->String(view.data(), static_cast<rj::SizeType>(view.size()));
+        writer_->String(std::string_view(view.data(), view.size()));
       } else {
         // Binary, encode to hexadecimal.
         writer_->String(HexEncode(arr.GetView(i)));
@@ -598,7 +596,7 @@ class ArrayWriter {
         const Decimal32 value(arr.GetValue(i));
         writer_->String(value.ToIntegerString());
       } else {
-        writer_->String(null_string, sizeof(null_string));
+        writer_->String(std::string_view(null_string));
       }
     }
   }
@@ -610,7 +608,7 @@ class ArrayWriter {
         const Decimal64 value(arr.GetValue(i));
         writer_->String(value.ToIntegerString());
       } else {
-        writer_->String(null_string, sizeof(null_string));
+        writer_->String(std::string_view(null_string));
       }
     }
   }
@@ -622,7 +620,7 @@ class ArrayWriter {
         const Decimal128 value(arr.GetValue(i));
         writer_->String(value.ToIntegerString());
       } else {
-        writer_->String(null_string, sizeof(null_string));
+        writer_->String(std::string_view(null_string));
       }
     }
   }
@@ -634,7 +632,7 @@ class ArrayWriter {
         const Decimal256 value(arr.GetValue(i));
         writer_->String(value.ToIntegerString());
       } else {
-        writer_->String(null_string, sizeof(null_string));
+        writer_->String(std::string_view(null_string));
       }
     }
   }
@@ -670,7 +668,7 @@ class ArrayWriter {
       // them exactly.
       ::arrow::internal::StringFormatter<typename CTypeTraits<T>::ArrowType> 
formatter;
       auto append = [this](std::string_view v) {
-        writer_->String(v.data(), static_cast<rj::SizeType>(v.size()));
+        writer_->String(std::string_view(v.data(), v.size()));
         return Status::OK();
       };
       for (int i = 0; i < length; ++i) {
@@ -692,7 +690,8 @@ class ArrayWriter {
       if (s.is_inline()) {
         writer_->Key("INLINED");
         if constexpr (ArrayType::TypeClass::is_utf8) {
-          writer_->String(reinterpret_cast<const char*>(s.inline_data()), 
s.size());
+          writer_->String(
+              std::string_view(reinterpret_cast<const char*>(s.inline_data()), 
s.size()));
         } else {
           writer_->String(HexEncode(s.inline_data(), s.size()));
         }
@@ -863,7 +862,7 @@ class ArrayWriter {
  private:
   const std::string& name_;
   const Array& array_;
-  RjWriter* writer_;
+  JsonWriter* writer_;
 };
 
 Result<TimeUnit::type> GetUnitFromString(const std::string& unit_str) {
@@ -2035,13 +2034,13 @@ Result<std::shared_ptr<RecordBatch>> ReadRecordBatch(
 }
 
 Status WriteSchema(const Schema& schema, const DictionaryFieldMapper& mapper,
-                   RjWriter* json_writer) {
+                   JsonWriter* json_writer) {
   SchemaWriter converter(schema, mapper, json_writer);
   return converter.Write();
 }
 
 Status WriteDictionary(int64_t id, const std::shared_ptr<Array>& dictionary,
-                       RjWriter* writer) {
+                       JsonWriter* writer) {
   writer->StartObject();
   writer->Key("id");
   writer->Int(static_cast<int32_t>(id));
@@ -2055,7 +2054,7 @@ Status WriteDictionary(int64_t id, const 
std::shared_ptr<Array>& dictionary,
   return Status::OK();
 }
 
-Status WriteRecordBatch(const RecordBatch& batch, RjWriter* writer) {
+Status WriteRecordBatch(const RecordBatch& batch, JsonWriter* writer) {
   writer->StartObject();
   writer->Key("count");
   writer->Int(static_cast<int32_t>(batch.num_rows()));
@@ -2076,7 +2075,7 @@ Status WriteRecordBatch(const RecordBatch& batch, 
RjWriter* writer) {
   return Status::OK();
 }
 
-Status WriteArray(const std::string& name, const Array& array, RjWriter* 
json_writer) {
+Status WriteArray(const std::string& name, const Array& array, JsonWriter* 
json_writer) {
   ArrayWriter converter(name, array, json_writer);
   return converter.Write();
 }
diff --git a/cpp/src/arrow/integration/json_internal.h 
b/cpp/src/arrow/integration/json_internal.h
index ac5bd2069d..b87c102ca9 100644
--- a/cpp/src/arrow/integration/json_internal.h
+++ b/cpp/src/arrow/integration/json_internal.h
@@ -36,9 +36,12 @@
 #include "arrow/util/visibility.h"
 
 namespace rj = arrow::rapidjson;
-using RjWriter = rj::Writer<rj::StringBuffer>;
-using RjArray = rj::Value::ConstArray;
 using RjObject = rj::Value::ConstObject;
+using RjArray = rj::Value::ConstArray;
+
+namespace arrow::json {
+class JsonWriter;
+}  // namespace arrow::json
 
 #define RETURN_NOT_FOUND(TOK, NAME, PARENT)              \
   if (NAME == (PARENT).MemberEnd()) {                    \
@@ -77,20 +80,20 @@ using RjObject = rj::Value::ConstObject;
 
 namespace arrow::internal::integration::json {
 
-/// \brief Append integration test Schema format to rapidjson writer
+/// \brief Append integration test Schema format to JSON writer
 ARROW_EXPORT
 Status WriteSchema(const Schema& schema, const ipc::DictionaryFieldMapper& 
mapper,
-                   RjWriter* writer);
+                   arrow::json::JsonWriter*);
 
 ARROW_EXPORT
 Status WriteDictionary(int64_t id, const std::shared_ptr<Array>& dictionary,
-                       RjWriter* writer);
+                       arrow::json::JsonWriter*);
 
 ARROW_EXPORT
-Status WriteRecordBatch(const RecordBatch& batch, RjWriter* writer);
+Status WriteRecordBatch(const RecordBatch& batch, arrow::json::JsonWriter*);
 
 ARROW_EXPORT
-Status WriteArray(const std::string& name, const Array& array, RjWriter* 
writer);
+Status WriteArray(const std::string& name, const Array& array, 
arrow::json::JsonWriter*);
 
 ARROW_EXPORT
 Result<std::shared_ptr<Schema>> ReadSchema(const rj::Value& json_obj, 
MemoryPool* pool,
diff --git a/cpp/src/arrow/integration/meson.build 
b/cpp/src/arrow/integration/meson.build
index 6437c380bb..17b6ea7449 100644
--- a/cpp/src/arrow/integration/meson.build
+++ b/cpp/src/arrow/integration/meson.build
@@ -20,7 +20,12 @@ install_headers(['json_integration.h'])
 exc = executable(
     'arrow-json-integration-test',
     sources: ['json_integration_test.cc'],
-    dependencies: [arrow_test_dep_no_main, rapidjson_dep, gflags_dep],
+    dependencies: [
+        arrow_test_dep_no_main,
+        rapidjson_dep,
+        simdjson_dep,
+        gflags_dep,
+    ],
 )
 
 arrow_c_data_integration_lib = library(
diff --git a/cpp/src/arrow/json/CMakeLists.txt 
b/cpp/src/arrow/json/CMakeLists.txt
index b5b47c0ed3..e7719c771c 100644
--- a/cpp/src/arrow/json/CMakeLists.txt
+++ b/cpp/src/arrow/json/CMakeLists.txt
@@ -21,13 +21,15 @@ add_arrow_test(test
                chunker_test.cc
                converter_test.cc
                from_string_test.cc
+               json_writer_internal_test.cc
+               object_parser_test.cc
                parser_test.cc
                reader_test.cc
-               object_parser_test.cc
                PREFIX
                "arrow-json"
                EXTRA_LINK_LIBS
-               RapidJSON)
+               RapidJSON
+               simdjson::simdjson)
 
 add_arrow_benchmark(parser_benchmark
                     PREFIX
diff --git a/cpp/src/arrow/json/json_writer_internal.cc 
b/cpp/src/arrow/json/json_writer_internal.cc
new file mode 100644
index 0000000000..11fe70a741
--- /dev/null
+++ b/cpp/src/arrow/json/json_writer_internal.cc
@@ -0,0 +1,118 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include "arrow/json/json_writer_internal.h"
+
+namespace arrow::json {
+
+void JsonWriter::StartObject() {
+  MaybeComma();
+  builder_.start_object();
+  needs_comma_ = false;
+}
+
+void JsonWriter::EndObject() {
+  builder_.end_object();
+  needs_comma_ = true;
+}
+
+void JsonWriter::StartArray() {
+  MaybeComma();
+  builder_.start_array();
+  needs_comma_ = false;
+}
+
+void JsonWriter::EndArray() {
+  builder_.end_array();
+  needs_comma_ = true;
+}
+
+void JsonWriter::Key(std::string_view key) {
+  MaybeComma();
+  builder_.escape_and_append_with_quotes(key);
+  builder_.append_colon();
+  needs_comma_ = false;
+}
+
+void JsonWriter::String(std::string_view value) {
+  MaybeComma();
+  builder_.escape_and_append_with_quotes(value);
+  needs_comma_ = true;
+}
+
+void JsonWriter::RawValue(std::string_view value) {
+  MaybeComma();
+  builder_.append_raw(value);
+  needs_comma_ = true;
+}
+
+void JsonWriter::Bool(bool value) {
+  MaybeComma();
+  builder_.append(value);
+  needs_comma_ = true;
+}
+
+void JsonWriter::Int(int32_t value) {
+  MaybeComma();
+  builder_.append(value);
+  needs_comma_ = true;
+}
+
+void JsonWriter::Int64(int64_t value) {
+  MaybeComma();
+  builder_.append(value);
+  needs_comma_ = true;
+}
+
+void JsonWriter::Uint(uint32_t value) {
+  MaybeComma();
+  builder_.append(value);
+  needs_comma_ = true;
+}
+
+void JsonWriter::Uint64(uint64_t value) {
+  MaybeComma();
+  builder_.append(value);
+  needs_comma_ = true;
+}
+
+void JsonWriter::Double(double value) {
+  MaybeComma();
+  builder_.append(value);
+  needs_comma_ = true;
+}
+
+void JsonWriter::Null() {
+  MaybeComma();
+  builder_.append_null();
+  needs_comma_ = true;
+}
+
+std::string_view JsonWriter::GetString() const { return 
builder_.view().value(); }
+
+void JsonWriter::Clear() {
+  builder_.clear();
+  needs_comma_ = false;
+}
+
+void JsonWriter::MaybeComma() {
+  if (needs_comma_) {
+    builder_.append_comma();
+  }
+}
+
+}  // namespace arrow::json
diff --git a/cpp/src/arrow/json/json_writer_internal.h 
b/cpp/src/arrow/json/json_writer_internal.h
new file mode 100644
index 0000000000..e4d6e49885
--- /dev/null
+++ b/cpp/src/arrow/json/json_writer_internal.h
@@ -0,0 +1,66 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#pragma once
+
+#include <simdjson.h>
+
+#include <cstdint>
+#include <string_view>
+
+#include "arrow/util/visibility.h"
+
+namespace arrow::json {
+
+class ARROW_EXPORT JsonWriter {
+ public:
+  JsonWriter() = default;
+
+  void StartObject();
+  void EndObject();
+
+  void StartArray();
+  void EndArray();
+
+  void Key(std::string_view key);
+
+  void String(std::string_view value);
+  void RawValue(std::string_view value);
+  void Bool(bool value);
+
+  void Int(int32_t value);
+  void Int64(int64_t value);
+
+  void Uint(uint32_t value);
+  void Uint64(uint64_t value);
+
+  void Double(double value);
+
+  void Null();
+
+  std::string_view GetString() const;
+
+  void Clear();
+
+ private:
+  void MaybeComma();
+
+  simdjson::builder::string_builder builder_;
+  bool needs_comma_ = false;
+};
+
+}  // namespace arrow::json
diff --git a/cpp/src/arrow/json/json_writer_internal_test.cc 
b/cpp/src/arrow/json/json_writer_internal_test.cc
new file mode 100644
index 0000000000..01e6d7ecb8
--- /dev/null
+++ b/cpp/src/arrow/json/json_writer_internal_test.cc
@@ -0,0 +1,152 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+#include <gtest/gtest.h>
+
+#include "arrow/json/json_writer_internal.h"
+
+namespace arrow::json {
+
+TEST(JsonWriter, SimpleObject) {
+  JsonWriter writer;
+
+  writer.StartObject();
+  writer.Key("a");
+  writer.Int(42);
+  writer.Key("b");
+  writer.String("hello");
+  writer.EndObject();
+
+  EXPECT_EQ(writer.GetString(), R"({"a":42,"b":"hello"})");
+}
+
+TEST(JsonWriter, Array) {
+  JsonWriter writer;
+
+  writer.StartArray();
+  writer.Int(1);
+  writer.Int(2);
+  writer.Int(3);
+  writer.EndArray();
+
+  EXPECT_EQ(writer.GetString(), "[1,2,3]");
+}
+
+TEST(JsonWriter, NestedObject) {
+  JsonWriter writer;
+
+  writer.StartObject();
+
+  writer.Key("child");
+  writer.StartObject();
+  writer.Key("x");
+  writer.Bool(true);
+  writer.EndObject();
+
+  writer.EndObject();
+
+  EXPECT_EQ(writer.GetString(), R"({"child":{"x":true}})");
+}
+
+TEST(JsonWriter, NullValue) {
+  JsonWriter writer;
+
+  writer.StartObject();
+  writer.Key("value");
+  writer.Null();
+  writer.EndObject();
+
+  EXPECT_EQ(writer.GetString(), R"({"value":null})");
+}
+
+TEST(JsonWriter, DoubleValue) {
+  JsonWriter writer;
+
+  writer.StartObject();
+  writer.Key("pi");
+  writer.Double(3.14);
+  writer.EndObject();
+
+  EXPECT_EQ(writer.GetString(), R"({"pi":3.14})");
+}
+
+TEST(JsonWriter, UnsignedValues) {
+  JsonWriter writer;
+
+  writer.StartObject();
+  writer.Key("u32");
+  writer.Uint(42);
+  writer.Key("u64");
+  writer.Uint64(1234567890123ULL);
+  writer.EndObject();
+
+  EXPECT_EQ(writer.GetString(), R"({"u32":42,"u64":1234567890123})");
+}
+
+TEST(JsonWriter, Int64Value) {
+  JsonWriter writer;
+
+  writer.StartObject();
+  writer.Key("i64");
+  writer.Int64(-1234567890123LL);
+  writer.EndObject();
+
+  EXPECT_EQ(writer.GetString(), R"({"i64":-1234567890123})");
+}
+
+TEST(JsonWriter, Clear) {
+  JsonWriter writer;
+
+  writer.StartObject();
+  writer.Key("a");
+  writer.Int(1);
+  writer.EndObject();
+
+  writer.Clear();
+
+  writer.StartArray();
+  writer.Int(5);
+  writer.EndArray();
+
+  EXPECT_EQ(writer.GetString(), "[5]");
+}
+
+TEST(JsonWriter, RawValue) {
+  JsonWriter writer;
+
+  writer.StartObject();
+  writer.Key("number");
+  writer.RawValue("123.456");
+  writer.EndObject();
+
+  ASSERT_EQ(writer.GetString(), R"({"number":123.456})");
+}
+
+TEST(JsonWriter, StringWithExplicitLength) {
+  JsonWriter writer;
+
+  const char value[] = {'a', 'b', 'c', 'd', 'e'};
+
+  writer.StartObject();
+  writer.Key("value");
+  writer.String(std::string_view(value, 3));
+  writer.EndObject();
+
+  ASSERT_EQ(writer.GetString(), R"({"value":"abc"})");
+}
+
+}  // namespace arrow::json
diff --git a/cpp/src/arrow/json/meson.build b/cpp/src/arrow/json/meson.build
index bc1567df9e..a2aa69ecf7 100644
--- a/cpp/src/arrow/json/meson.build
+++ b/cpp/src/arrow/json/meson.build
@@ -24,6 +24,7 @@ exc = executable(
         'from_string_test.cc',
         'parser_test.cc',
         'reader_test.cc',
+        'json_writer_internal_test.cc',
     ],
     dependencies: [arrow_test_dep, rapidjson_dep],
 )
diff --git a/cpp/src/arrow/json/reader_test.cc 
b/cpp/src/arrow/json/reader_test.cc
index 98bdac96c2..2aca602ae9 100644
--- a/cpp/src/arrow/json/reader_test.cc
+++ b/cpp/src/arrow/json/reader_test.cc
@@ -39,7 +39,7 @@ namespace json {
 
 using std::string_view;
 
-using internal::checked_cast;
+using arrow::internal::checked_cast;
 
 static Result<std::shared_ptr<Table>> ReadToTable(std::string json,
                                                   const ReadOptions& 
read_options,
@@ -597,7 +597,7 @@ class StreamingReaderTestBase {
     return out;
   }
 
-  internal::Executor* executor_ = nullptr;
+  arrow::internal::Executor* executor_ = nullptr;
   ParseOptions parse_options_ = ParseOptions::Defaults();
   ReadOptions read_options_ = ReadOptions::Defaults();
   io::IOContext io_context_ = io::default_io_context();
@@ -965,7 +965,7 @@ TEST_F(AsyncStreamingReaderTest, AsyncReentrancy) {
 
   ASSERT_FINISHES_OK_AND_ASSIGN(auto results, All(std::move(futures)));
   EXPECT_EQ(reader->bytes_processed(), expected.json_size);
-  ASSERT_OK_AND_ASSIGN(auto batches, 
internal::UnwrapOrRaise(std::move(results)));
+  ASSERT_OK_AND_ASSIGN(auto batches, 
arrow::internal::UnwrapOrRaise(std::move(results)));
   AssertBatchSequenceEquals(expected.batches, batches);
 }
 
@@ -989,7 +989,7 @@ TEST_F(AsyncStreamingReaderTest, FuturesOutliveReader) {
   }
 
   ASSERT_FINISHES_OK_AND_ASSIGN(auto results, All(std::move(futures)));
-  ASSERT_OK_AND_ASSIGN(auto batches, 
internal::UnwrapOrRaise(std::move(results)));
+  ASSERT_OK_AND_ASSIGN(auto batches, 
arrow::internal::UnwrapOrRaise(std::move(results)));
   AssertBatchSequenceEquals(expected.batches, batches);
 }
 
@@ -1009,7 +1009,7 @@ TEST_F(AsyncStreamingReaderTest, StressBufferedReads) {
   }
 
   ASSERT_FINISHES_OK_AND_ASSIGN(auto results, All(std::move(futures)));
-  ASSERT_OK_AND_ASSIGN(auto batches, internal::UnwrapOrRaise(results));
+  ASSERT_OK_AND_ASSIGN(auto batches, arrow::internal::UnwrapOrRaise(results));
   AssertBatchSequenceEquals(expected.batches, batches);
 }
 
@@ -1023,7 +1023,7 @@ TEST_F(AsyncStreamingReaderTest, 
StressSharedIoAndCpuExecutor) {
   read_options_.block_size = expected.block_size;
 
   // Force the serial -> parallel pipeline to contend for a single thread
-  ASSERT_OK_AND_ASSIGN(auto thread_pool, internal::ThreadPool::Make(1));
+  ASSERT_OK_AND_ASSIGN(auto thread_pool, arrow::internal::ThreadPool::Make(1));
   io_context_ = io::IOContext(thread_pool.get());
   executor_ = thread_pool.get();
 
diff --git a/cpp/src/arrow/meson.build b/cpp/src/arrow/meson.build
index 831bc12180..ef044f6c42 100644
--- a/cpp/src/arrow/meson.build
+++ b/cpp/src/arrow/meson.build
@@ -510,13 +510,14 @@ if needs_json
                 'extension/opaque.cc',
                 'extension/tensor_internal.cc',
                 'extension/variable_shape_tensor.cc',
-                'json/options.cc',
                 'json/chunked_builder.cc',
                 'json/chunker.cc',
                 'json/converter.cc',
                 'json/from_string.cc',
+                'json/json_writer_internal.cc',
                 'json/object_parser.cc',
                 'json/object_writer.cc',
+                'json/options.cc',
                 'json/parser.cc',
                 'json/reader.cc',
             ],
diff --git a/dev/tasks/tasks.yml b/dev/tasks/tasks.yml
index 89d239a0a2..af8b319dca 100644
--- a/dev/tasks/tasks.yml
+++ b/dev/tasks/tasks.yml
@@ -645,7 +645,7 @@ tasks:
     ci: github
     template: docker-tests/github.linux.yml
     params:
-      flags: '-e ARROW_DEPENDENCY_SOURCE=SYSTEM -e ARROW_GCS=OFF -e 
xsimd_SOURCE=BUNDLED'
+      flags: '-e ARROW_DEPENDENCY_SOURCE=SYSTEM -e ARROW_GCS=OFF -e 
simdjson_SOURCE=BUNDLED -e xsimd_SOURCE=BUNDLED'
       image: ubuntu-r-only-r
 
   test-r-dev-duckdb:

Reply via email to