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

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


The following commit(s) were added to refs/heads/main by this push:
     new c3dc4605e GH-1302: Add support for the arrow.json extension type 
(#1303)
c3dc4605e is described below

commit c3dc4605e47b31fea9870d81f40c498f57660dd4
Author: Xin Sun <[email protected]>
AuthorDate: Sun Oct 4 23:05:28 2026 +0800

    GH-1302: Add support for the arrow.json extension type (#1303)
    
    Adds `JsonType` and `JsonVector` for the canonical `arrow.json`
    extension, backed by `Utf8`, `LargeUtf8`, or `Utf8View`.
    
    Includes metadata handling, registration, and vector transfers that
    preserve the extension type. IPC loading and unloading now inspect the
    underlying vector when counting view buffers, so JSON backed by
    `Utf8View` can round-trip correctly.
    
    Closes #1302.
---
 .../org/apache/arrow/c/StructVectorLoader.java     |   6 +-
 .../org/apache/arrow/c/StructVectorUnloader.java   |   6 +-
 .../java/org/apache/arrow/c/RoundtripTest.java     |  70 ++++++
 .../apache/arrow/vector/ExtensionTypeVector.java   |  18 ++
 .../java/org/apache/arrow/vector/VectorLoader.java |   5 +-
 .../org/apache/arrow/vector/VectorUnloader.java    |   5 +-
 .../apache/arrow/vector/extension/JsonType.java    | 132 +++++++++++
 .../apache/arrow/vector/extension/JsonVector.java  | 126 ++++++++++
 .../java/org/apache/arrow/vector/TestJsonType.java | 260 +++++++++++++++++++++
 .../src/test/resources/pyarrow_json_schema.arrow   | Bin 0 -> 728 bytes
 10 files changed, 620 insertions(+), 8 deletions(-)

diff --git a/c/src/main/java/org/apache/arrow/c/StructVectorLoader.java 
b/c/src/main/java/org/apache/arrow/c/StructVectorLoader.java
index 6a962cc34..2b201c777 100644
--- a/c/src/main/java/org/apache/arrow/c/StructVectorLoader.java
+++ b/c/src/main/java/org/apache/arrow/c/StructVectorLoader.java
@@ -26,6 +26,7 @@ import org.apache.arrow.memory.ArrowBuf;
 import org.apache.arrow.memory.BufferAllocator;
 import org.apache.arrow.util.Collections2;
 import org.apache.arrow.vector.BaseVariableWidthViewVector;
+import org.apache.arrow.vector.ExtensionTypeVector;
 import org.apache.arrow.vector.FieldVector;
 import org.apache.arrow.vector.TypeLayout;
 import org.apache.arrow.vector.complex.StructVector;
@@ -124,9 +125,10 @@ public class StructVectorLoader {
       Iterator<Long> variadicBufferCounts) {
     checkArgument(nodes.hasNext(), "no more field nodes for field %s and 
vector %s", field, vector);
     ArrowFieldNode fieldNode = nodes.next();
-    // variadicBufferLayoutCount will be 0 for vectors of a type except 
BaseVariableWidthViewVector
+    FieldVector storageVector = ExtensionTypeVector.getStorageVector(vector);
+    // Only view storage has variadic buffers.
     long variadicBufferLayoutCount = 0;
-    if (vector instanceof BaseVariableWidthViewVector) {
+    if (storageVector instanceof BaseVariableWidthViewVector) {
       if (variadicBufferCounts.hasNext()) {
         variadicBufferLayoutCount = variadicBufferCounts.next();
       } else {
diff --git a/c/src/main/java/org/apache/arrow/c/StructVectorUnloader.java 
b/c/src/main/java/org/apache/arrow/c/StructVectorUnloader.java
index 6f094b92c..2ce06284f 100644
--- a/c/src/main/java/org/apache/arrow/c/StructVectorUnloader.java
+++ b/c/src/main/java/org/apache/arrow/c/StructVectorUnloader.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
 import java.util.List;
 import org.apache.arrow.memory.ArrowBuf;
 import org.apache.arrow.vector.BaseVariableWidthViewVector;
+import org.apache.arrow.vector.ExtensionTypeVector;
 import org.apache.arrow.vector.FieldVector;
 import org.apache.arrow.vector.TypeLayout;
 import org.apache.arrow.vector.complex.StructVector;
@@ -103,12 +104,13 @@ public class StructVectorUnloader {
       List<Long> variadicBufferCounts) {
     nodes.add(
         new ArrowFieldNode(vector.getValueCount(), includeNullCount ? 
vector.getNullCount() : -1));
+    FieldVector storageVector = ExtensionTypeVector.getStorageVector(vector);
     List<ArrowBuf> fieldBuffers = vector.getFieldBuffers();
-    long variadicBufferCount = getVariadicBufferCount(vector);
+    long variadicBufferCount = getVariadicBufferCount(storageVector);
     int expectedBufferCount =
         (int) (TypeLayout.getTypeBufferCount(vector.getField().getType()) + 
variadicBufferCount);
     // only update variadicBufferCounts for vectors that have variadic buffers
-    if (vector instanceof BaseVariableWidthViewVector) {
+    if (storageVector instanceof BaseVariableWidthViewVector) {
       variadicBufferCounts.add(variadicBufferCount);
     }
     if (fieldBuffers.size() != expectedBufferCount) {
diff --git a/c/src/test/java/org/apache/arrow/c/RoundtripTest.java 
b/c/src/test/java/org/apache/arrow/c/RoundtripTest.java
index 7a301cc3e..ed1690a9e 100644
--- a/c/src/test/java/org/apache/arrow/c/RoundtripTest.java
+++ b/c/src/test/java/org/apache/arrow/c/RoundtripTest.java
@@ -43,6 +43,7 @@ import org.apache.arrow.vector.DateDayVector;
 import org.apache.arrow.vector.DateMilliVector;
 import org.apache.arrow.vector.DecimalVector;
 import org.apache.arrow.vector.DurationVector;
+import org.apache.arrow.vector.ExtensionTypeVector;
 import org.apache.arrow.vector.FieldVector;
 import org.apache.arrow.vector.FixedSizeBinaryVector;
 import org.apache.arrow.vector.Float2Vector;
@@ -76,6 +77,7 @@ import org.apache.arrow.vector.UuidVector;
 import org.apache.arrow.vector.ValueVector;
 import org.apache.arrow.vector.VarBinaryVector;
 import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.VariableWidthFieldVector;
 import org.apache.arrow.vector.VectorSchemaRoot;
 import org.apache.arrow.vector.ViewVarBinaryVector;
 import org.apache.arrow.vector.ViewVarCharVector;
@@ -91,6 +93,8 @@ import org.apache.arrow.vector.complex.RunEndEncodedVector;
 import org.apache.arrow.vector.complex.StructVector;
 import org.apache.arrow.vector.complex.UnionVector;
 import org.apache.arrow.vector.complex.impl.UnionMapWriter;
+import org.apache.arrow.vector.extension.JsonType;
+import org.apache.arrow.vector.extension.OpaqueType;
 import org.apache.arrow.vector.extension.UuidType;
 import org.apache.arrow.vector.holders.IntervalDayHolder;
 import org.apache.arrow.vector.holders.NullableLargeVarBinaryHolder;
@@ -100,6 +104,7 @@ import org.apache.arrow.vector.types.TimeUnit;
 import org.apache.arrow.vector.types.Types.MinorType;
 import org.apache.arrow.vector.types.pojo.ArrowType;
 import org.apache.arrow.vector.types.pojo.ArrowType.ExtensionType;
+import org.apache.arrow.vector.types.pojo.ExtensionTypeRegistry;
 import org.apache.arrow.vector.types.pojo.Field;
 import org.apache.arrow.vector.types.pojo.FieldType;
 import org.apache.arrow.vector.types.pojo.Schema;
@@ -107,6 +112,9 @@ import org.apache.arrow.vector.util.TransferPair;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 public class RoundtripTest {
   private static final String EMPTY_SCHEMA_PATH = "";
@@ -848,6 +856,68 @@ public class RoundtripTest {
     }
   }
 
+  static Stream<Arguments> viewExtensionCases() {
+    byte[][] inlined = {
+      "{}".getBytes(StandardCharsets.UTF_8), null, 
"null".getBytes(StandardCharsets.UTF_8)
+    };
+    byte[][] external = {
+      "{\"message\":\"a JSON value longer than twelve 
bytes\"}".getBytes(StandardCharsets.UTF_8),
+      null
+    };
+    return Stream.of(
+            new JsonType(ArrowType.Utf8View.INSTANCE),
+            new OpaqueType(ArrowType.Utf8View.INSTANCE, "json", "test"),
+            new OpaqueType(ArrowType.BinaryView.INSTANCE, "binary", "test"))
+        .flatMap(
+            type ->
+                Stream.of(
+                    Arguments.of(type, new byte[0][], 3),
+                    Arguments.of(type, inlined, 3),
+                    Arguments.of(type, external, 4)));
+  }
+
+  @ParameterizedTest
+  @MethodSource("viewExtensionCases")
+  public void testViewExtensionTypeVector(ExtensionType type, byte[][] values, 
int bufferCount) {
+    ExtensionType previous = 
ExtensionTypeRegistry.lookup(type.extensionName());
+    ExtensionTypeRegistry.register(type);
+    Schema schema = new Schema(Collections.singletonList(Field.nullable("a", 
type)));
+    try (VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator)) {
+      ExtensionTypeVector<?> vector = (ExtensionTypeVector<?>) 
root.getVector("a");
+      setVector((VariableWidthFieldVector) vector.getUnderlyingVector(), 
values);
+      root.setRowCount(values.length);
+
+      try (ArrowSchema arrowSchema = ArrowSchema.allocateNew(allocator);
+          ArrowArray arrowArray = ArrowArray.allocateNew(allocator)) {
+        Data.exportVector(allocator, vector, null, arrowArray, arrowSchema);
+        assertEquals(bufferCount, arrowArray.snapshot().n_buffers);
+        try (FieldVector imported =
+            Data.importVector(childAllocator, arrowArray, arrowSchema, null)) {
+          assertViewExtensionVector(vector, imported);
+        }
+      }
+
+      try (VectorSchemaRoot imported = vectorSchemaRootRoundtrip(root)) {
+        assertEquals(root.getSchema(), imported.getSchema());
+        assertEquals(root.getRowCount(), imported.getRowCount());
+        assertViewExtensionVector(vector, imported.getVector("a"));
+      }
+    } finally {
+      ExtensionTypeRegistry.unregister(type);
+      if (previous != null) {
+        ExtensionTypeRegistry.register(previous);
+      }
+    }
+  }
+
+  private void assertViewExtensionVector(ExtensionTypeVector<?> expected, 
FieldVector actual) {
+    assertEquals(expected.getField(), actual.getField());
+    ExtensionTypeVector<?> imported = 
assertInstanceOf(ExtensionTypeVector.class, actual);
+    assertTrue(
+        VectorEqualsVisitor.vectorEquals(
+            expected.getUnderlyingVector(), imported.getUnderlyingVector()));
+  }
+
   @Test
   public void testVectorSchemaRoot() {
     VectorSchemaRoot imported;
diff --git 
a/vector/src/main/java/org/apache/arrow/vector/ExtensionTypeVector.java 
b/vector/src/main/java/org/apache/arrow/vector/ExtensionTypeVector.java
index 3762fecd0..8249d9346 100644
--- a/vector/src/main/java/org/apache/arrow/vector/ExtensionTypeVector.java
+++ b/vector/src/main/java/org/apache/arrow/vector/ExtensionTypeVector.java
@@ -76,6 +76,24 @@ public abstract class ExtensionTypeVector<T extends 
ValueVector & FieldVector>
     return underlyingVector;
   }
 
+  /** Get the storage vector, unwrapping extension vectors. */
+  public static FieldVector getStorageVector(FieldVector vector) {
+    while (vector instanceof ExtensionTypeVector) {
+      vector = ((ExtensionTypeVector<?>) vector).getUnderlyingVector();
+    }
+    return vector;
+  }
+
+  @Override
+  public int getExportedCDataBufferCount() {
+    return this.underlyingVector.getExportedCDataBufferCount();
+  }
+
+  @Override
+  public void exportCDataBuffers(List<ArrowBuf> buffers, ArrowBuf buffersPtr, 
long nullValue) {
+    this.underlyingVector.exportCDataBuffers(buffers, buffersPtr, nullValue);
+  }
+
   @Override
   public void allocateNew() throws OutOfMemoryException {
     this.underlyingVector.allocateNew();
diff --git a/vector/src/main/java/org/apache/arrow/vector/VectorLoader.java 
b/vector/src/main/java/org/apache/arrow/vector/VectorLoader.java
index 9b9a89034..8233fb4d5 100644
--- a/vector/src/main/java/org/apache/arrow/vector/VectorLoader.java
+++ b/vector/src/main/java/org/apache/arrow/vector/VectorLoader.java
@@ -106,11 +106,12 @@ public class VectorLoader {
       Iterator<ArrowFieldNode> nodes,
       CompressionCodec codec,
       Iterator<Long> variadicBufferCounts) {
+    FieldVector storageVector = ExtensionTypeVector.getStorageVector(vector);
     checkArgument(nodes.hasNext(), "no more field nodes for field %s and 
vector %s", field, vector);
     ArrowFieldNode fieldNode = nodes.next();
-    // variadicBufferLayoutCount will be 0 for vectors of a type except 
BaseVariableWidthViewVector
+    // Only view storage has variadic buffers.
     long variadicBufferLayoutCount = 0;
-    if (vector instanceof BaseVariableWidthViewVector) {
+    if (storageVector instanceof BaseVariableWidthViewVector) {
       if (variadicBufferCounts.hasNext()) {
         variadicBufferLayoutCount = variadicBufferCounts.next();
       } else {
diff --git a/vector/src/main/java/org/apache/arrow/vector/VectorUnloader.java 
b/vector/src/main/java/org/apache/arrow/vector/VectorUnloader.java
index 342f210b8..4782b3f53 100644
--- a/vector/src/main/java/org/apache/arrow/vector/VectorUnloader.java
+++ b/vector/src/main/java/org/apache/arrow/vector/VectorUnloader.java
@@ -104,14 +104,15 @@ public class VectorUnloader {
       List<ArrowFieldNode> nodes,
       List<ArrowBuf> buffers,
       List<Long> variadicBufferCounts) {
+    FieldVector storageVector = ExtensionTypeVector.getStorageVector(vector);
     nodes.add(
         new ArrowFieldNode(vector.getValueCount(), includeNullCount ? 
vector.getNullCount() : -1));
     List<ArrowBuf> fieldBuffers = vector.getFieldBuffers();
-    long variadicBufferCount = getVariadicBufferCount(vector);
+    long variadicBufferCount = getVariadicBufferCount(storageVector);
     int expectedBufferCount =
         (int) (TypeLayout.getTypeBufferCount(vector.getField().getType()) + 
variadicBufferCount);
     // only update variadicBufferCounts for vectors that have variadic buffers
-    if (vector instanceof BaseVariableWidthViewVector) {
+    if (storageVector instanceof BaseVariableWidthViewVector) {
       variadicBufferCounts.add(variadicBufferCount);
     }
     if (fieldBuffers.size() != expectedBufferCount) {
diff --git 
a/vector/src/main/java/org/apache/arrow/vector/extension/JsonType.java 
b/vector/src/main/java/org/apache/arrow/vector/extension/JsonType.java
new file mode 100644
index 000000000..be3ebb4f7
--- /dev/null
+++ b/vector/src/main/java/org/apache/arrow/vector/extension/JsonType.java
@@ -0,0 +1,132 @@
+/*
+ * 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.
+ */
+package org.apache.arrow.vector.extension;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.Collections;
+import java.util.Objects;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.ExtensionTypeRegistry;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.FieldType;
+
+/**
+ * Canonical extension type for UTF-8 encoded RFC 8259 JSON values.
+ *
+ * <p>The storage type is {@link ArrowType.Utf8}, {@link ArrowType.LargeUtf8}, 
or {@link
+ * ArrowType.Utf8View}. Values use the corresponding string vector; this type 
does not parse or
+ * validate individual JSON values.
+ *
+ * <p>Register the type before reading schemas containing {@code arrow.json}:
+ *
+ * <pre>{@code
+ * JsonType.ensureRegistered();
+ * Field field = Field.nullable("json", new JsonType(ArrowType.Utf8.INSTANCE));
+ * try (JsonVector vector = (JsonVector) field.createVector(allocator)) {
+ *   VarCharVector storage = (VarCharVector) vector.getUnderlyingVector();
+ *   storage.setSafe(0, 
"{}".getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ *   vector.setValueCount(1);
+ *   Text value = vector.getObject(0);
+ * }
+ * }</pre>
+ */
+public class JsonType extends ArrowType.ExtensionType {
+  public static final String EXTENSION_NAME = "arrow.json";
+  private static final ObjectMapper MAPPER =
+      new 
ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
+  private final ArrowType storageType;
+
+  /** Register a prototype that can deserialize all supported JSON storage 
types. */
+  public static void ensureRegistered() {
+    ExtensionTypeRegistry.register(new JsonType(ArrowType.Utf8.INSTANCE));
+  }
+
+  /**
+   * Create a JSON type backed by the specified string type.
+   *
+   * @param storageType Utf8, LargeUtf8, or Utf8View
+   * @throws IllegalArgumentException if the storage type is not a supported 
string type
+   */
+  public JsonType(ArrowType storageType) {
+    Objects.requireNonNull(storageType, "storageType");
+    if (!(storageType instanceof ArrowType.Utf8)
+        && !(storageType instanceof ArrowType.LargeUtf8)
+        && !(storageType instanceof ArrowType.Utf8View)) {
+      throw new IllegalArgumentException(
+          "arrow.json requires Utf8, LargeUtf8, or Utf8View storage, got " + 
storageType);
+    }
+    this.storageType = storageType;
+  }
+
+  @Override
+  public ArrowType storageType() {
+    return storageType;
+  }
+
+  @Override
+  public String extensionName() {
+    return EXTENSION_NAME;
+  }
+
+  @Override
+  public boolean extensionEquals(ExtensionType other) {
+    return other instanceof JsonType && 
storageType.equals(other.storageType());
+  }
+
+  @Override
+  public String serialize() {
+    return "";
+  }
+
+  @Override
+  public ArrowType deserialize(ArrowType storageType, String serializedData) {
+    JsonType type = new JsonType(storageType);
+    if (serializedData == null) {
+      throw new InvalidExtensionMetadataException("arrow.json metadata must 
not be null");
+    }
+    if (!serializedData.isEmpty()) {
+      try {
+        JsonNode metadata = MAPPER.readTree(serializedData);
+        if (metadata == null || !metadata.isObject()) {
+          throw new InvalidExtensionMetadataException("arrow.json metadata 
must be a JSON object");
+        }
+      } catch (JsonProcessingException e) {
+        throw new InvalidExtensionMetadataException("arrow.json metadata is 
invalid", e);
+      }
+    }
+    return type;
+  }
+
+  @Override
+  public boolean isComplex() {
+    return false;
+  }
+
+  @Override
+  public FieldVector getNewVector(String name, FieldType fieldType, 
BufferAllocator allocator) {
+    Field field = new Field(name, fieldType, Collections.emptyList());
+    FieldType storageFieldType =
+        new FieldType(fieldType.isNullable(), storageType, 
fieldType.getDictionary(), null);
+    FieldVector storage = storageFieldType.createNewSingleVector(name, 
allocator, null);
+    return new JsonVector(field, allocator, storage);
+  }
+}
diff --git 
a/vector/src/main/java/org/apache/arrow/vector/extension/JsonVector.java 
b/vector/src/main/java/org/apache/arrow/vector/extension/JsonVector.java
new file mode 100644
index 000000000..76c8579d0
--- /dev/null
+++ b/vector/src/main/java/org/apache/arrow/vector/extension/JsonVector.java
@@ -0,0 +1,126 @@
+/*
+ * 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.
+ */
+package org.apache.arrow.vector.extension;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.util.hash.ArrowBufHasher;
+import org.apache.arrow.vector.ExtensionTypeVector;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.ValueIterableVector;
+import org.apache.arrow.vector.ValueVector;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.util.CallBack;
+import org.apache.arrow.vector.util.Text;
+import org.apache.arrow.vector.util.TransferPair;
+
+/**
+ * A JSON extension vector backed by a string vector.
+ *
+ * <p>Use {@link Field#createVector(BufferAllocator)} with a {@link JsonType} 
field to create an
+ * instance. Write UTF-8 JSON through {@link #getUnderlyingVector()}; values 
are not parsed or
+ * validated.
+ */
+public class JsonVector extends ExtensionTypeVector<FieldVector>
+    implements ValueIterableVector<Text> {
+  private final Field field;
+
+  JsonVector(Field field, BufferAllocator allocator, FieldVector 
underlyingVector) {
+    super(field, allocator, underlyingVector);
+    this.field = field;
+  }
+
+  @Override
+  public Field getField() {
+    return field;
+  }
+
+  @Override
+  public Text getObject(int index) {
+    return (Text) getUnderlyingVector().getObject(index);
+  }
+
+  @Override
+  public TransferPair getTransferPair(BufferAllocator allocator) {
+    return getTransferPair(field, allocator);
+  }
+
+  @Override
+  public TransferPair getTransferPair(String name, BufferAllocator allocator) {
+    return getTransferPair(new Field(name, field.getFieldType(), 
field.getChildren()), allocator);
+  }
+
+  @Override
+  public TransferPair getTransferPair(String name, BufferAllocator allocator, 
CallBack callBack) {
+    return getTransferPair(name, allocator);
+  }
+
+  @Override
+  public TransferPair getTransferPair(Field targetField, BufferAllocator 
allocator) {
+    return makeTransferPair(targetField.createVector(allocator));
+  }
+
+  @Override
+  public TransferPair getTransferPair(
+      Field targetField, BufferAllocator allocator, CallBack callBack) {
+    return getTransferPair(targetField, allocator);
+  }
+
+  @Override
+  public TransferPair makeTransferPair(ValueVector target) {
+    return new TransferImpl((JsonVector) target);
+  }
+
+  @Override
+  public int hashCode(int index) {
+    return hashCode(index, null);
+  }
+
+  @Override
+  public int hashCode(int index, ArrowBufHasher hasher) {
+    return getUnderlyingVector().hashCode(index, hasher);
+  }
+
+  private class TransferImpl implements TransferPair {
+    private final JsonVector to;
+    private final TransferPair storagePair;
+
+    TransferImpl(JsonVector to) {
+      this.to = to;
+      this.storagePair = 
getUnderlyingVector().makeTransferPair(to.getUnderlyingVector());
+    }
+
+    @Override
+    public void transfer() {
+      storagePair.transfer();
+    }
+
+    @Override
+    public void splitAndTransfer(int startIndex, int length) {
+      storagePair.splitAndTransfer(startIndex, length);
+    }
+
+    @Override
+    public JsonVector getTo() {
+      return to;
+    }
+
+    @Override
+    public void copyValueSafe(int fromIndex, int toIndex) {
+      storagePair.copyValueSafe(fromIndex, toIndex);
+    }
+  }
+}
diff --git a/vector/src/test/java/org/apache/arrow/vector/TestJsonType.java 
b/vector/src/test/java/org/apache/arrow/vector/TestJsonType.java
new file mode 100644
index 000000000..ebad0d148
--- /dev/null
+++ b/vector/src/test/java/org/apache/arrow/vector/TestJsonType.java
@@ -0,0 +1,260 @@
+/*
+ * 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.
+ */
+package org.apache.arrow.vector;
+
+import static 
org.apache.arrow.vector.testing.ValueVectorDataPopulator.setVector;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.stream.Stream;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.vector.extension.InvalidExtensionMetadataException;
+import org.apache.arrow.vector.extension.JsonType;
+import org.apache.arrow.vector.extension.JsonVector;
+import org.apache.arrow.vector.ipc.ArrowStreamReader;
+import org.apache.arrow.vector.ipc.ArrowStreamWriter;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.ExtensionTypeRegistry;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.FieldType;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.apache.arrow.vector.util.Text;
+import org.apache.arrow.vector.util.TransferPair;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class TestJsonType {
+  BufferAllocator allocator;
+  ArrowType.ExtensionType previousType;
+
+  @BeforeEach
+  void beforeEach() {
+    allocator = new RootAllocator();
+    previousType = ExtensionTypeRegistry.lookup(JsonType.EXTENSION_NAME);
+    JsonType.ensureRegistered();
+  }
+
+  @AfterEach
+  void afterEach() {
+    ExtensionTypeRegistry.unregister(new JsonType(ArrowType.Utf8.INSTANCE));
+    if (previousType != null) {
+      ExtensionTypeRegistry.register(previousType);
+    }
+    allocator.close();
+  }
+
+  static Stream<ArrowType> storageTypes() {
+    return Stream.of(
+        ArrowType.Utf8.INSTANCE, ArrowType.LargeUtf8.INSTANCE, 
ArrowType.Utf8View.INSTANCE);
+  }
+
+  @ParameterizedTest
+  @MethodSource("storageTypes")
+  void testRoundTrip(ArrowType storage) {
+    JsonType type = new JsonType(storage);
+    assertEquals("arrow.json", type.extensionName());
+    assertEquals(storage, type.storageType());
+    assertFalse(type.isComplex());
+    assertEquals("", type.serialize());
+    assertEquals(type, type.deserialize(storage, type.serialize()));
+    assertNotEquals(
+        type,
+        new JsonType(
+            storage instanceof ArrowType.Utf8
+                ? ArrowType.LargeUtf8.INSTANCE
+                : ArrowType.Utf8.INSTANCE));
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = {"", "{}", " { } ", "{\"future\": 1}"})
+  void testDeserializeValid(String metadata) {
+    JsonType type = new JsonType(ArrowType.Utf8.INSTANCE);
+    assertEquals(type, type.deserialize(type.storageType(), metadata));
+  }
+
+  @ParameterizedTest
+  @NullSource
+  @ValueSource(strings = {" ", "null", "[]", "1", "true", "\"json\"", "{", "{} 
{}", "{} trailing"})
+  void testInvalidMetadata(String metadata) {
+    assertThrows(
+        InvalidExtensionMetadataException.class,
+        () -> new 
JsonType(ArrowType.Utf8.INSTANCE).deserialize(ArrowType.Utf8.INSTANCE, 
metadata));
+  }
+
+  @Test
+  void testInvalidStorage() {
+    for (ArrowType storage :
+        new ArrowType[] {
+          ArrowType.Binary.INSTANCE,
+          ArrowType.LargeBinary.INSTANCE,
+          ArrowType.BinaryView.INSTANCE,
+          ArrowType.Null.INSTANCE,
+          new ArrowType.Int(32, true)
+        }) {
+      assertThrows(IllegalArgumentException.class, () -> new 
JsonType(storage));
+      assertThrows(
+          IllegalArgumentException.class,
+          () -> new JsonType(ArrowType.Utf8.INSTANCE).deserialize(storage, 
""));
+    }
+  }
+
+  @ParameterizedTest
+  @MethodSource("storageTypes")
+  void testSchemaRoundTrip(ArrowType storage) {
+    for (boolean nullable : new boolean[] {false, true}) {
+      Field field = field(storage, nullable);
+      Schema schema = new Schema(Collections.singletonList(field));
+      assertEquals(schema, 
Schema.deserializeMessage(ByteBuffer.wrap(schema.serializeAsMessage())));
+    }
+  }
+
+  // Generated with PyArrow 24.0.0 using pa.schema([pa.field("json", 
pa.json_(t),
+  // nullable=False, metadata={"custom": "preserved"}) for t in
+  // [pa.string(), pa.large_string(), pa.string_view()]]).serialize().
+  @Test
+  void testPyArrowSchema() throws IOException {
+    Schema schema;
+    try (InputStream input = 
getClass().getResourceAsStream("/pyarrow_json_schema.arrow")) {
+      schema = 
Schema.deserializeMessage(ByteBuffer.wrap(input.readAllBytes()));
+    }
+    ArrowType[] storage = storageTypes().toArray(ArrowType[]::new);
+    for (int i = 0; i < storage.length; i++) {
+      assertEquals(field(storage[i], false), schema.getFields().get(i));
+    }
+  }
+
+  private static Field field(ArrowType storage, boolean nullable) {
+    return new Field(
+        "json",
+        new FieldType(
+            nullable, new JsonType(storage), null, 
Collections.singletonMap("custom", "preserved")),
+        Collections.emptyList());
+  }
+
+  @ParameterizedTest
+  @MethodSource("storageTypes")
+  void testTransfer(ArrowType storage) {
+    Field field = field(storage, true);
+    try (JsonVector source = (JsonVector) field.createVector(allocator)) {
+      byte[] bytes =
+          "{\"key\":\"value longer than twelve 
bytes\"}".getBytes(StandardCharsets.UTF_8);
+      setVector((VariableWidthFieldVector) source.getUnderlyingVector(), null, 
bytes, null);
+      TransferPair split = source.getTransferPair("copy", allocator);
+      try (JsonVector target = assertInstanceOf(JsonVector.class, 
split.getTo())) {
+        split.splitAndTransfer(1, 2);
+        assertEquals("copy", target.getName());
+        assertEquals(field.getFieldType(), target.getField().getFieldType());
+        assertEquals(new Text(bytes), target.getObject(0));
+        assertTrue(target.isNull(1));
+      }
+      try (JsonVector target = (JsonVector) field.createVector(allocator)) {
+        TransferPair copy = source.makeTransferPair(target);
+        copy.copyValueSafe(1, 0);
+        copy.copyValueSafe(2, 1);
+        target.setValueCount(2);
+        assertEquals(new Text(bytes), target.getObject(0));
+        assertTrue(target.isNull(1));
+      }
+      TransferPair transfer = source.getTransferPair(allocator);
+      try (JsonVector target = assertInstanceOf(JsonVector.class, 
transfer.getTo())) {
+        transfer.transfer();
+        assertEquals(field, target.getField());
+        assertTrue(target.isNull(0));
+        assertEquals(new Text(bytes), target.getObject(1));
+        assertTrue(target.isNull(2));
+        assertEquals(3, target.getValueCount());
+        assertEquals(0, source.getValueCount());
+      }
+    }
+  }
+
+  @ParameterizedTest
+  @MethodSource("storageTypes")
+  void testVectorIpcRoundTrip(ArrowType storage) throws IOException {
+    Field field = field(storage, true);
+    byte[] serialized = writeStream(field);
+    try (ArrowStreamReader reader =
+        new ArrowStreamReader(new ByteArrayInputStream(serialized), 
allocator)) {
+      assertTrue(reader.loadNextBatch());
+      JsonVector vector =
+          assertInstanceOf(JsonVector.class, 
reader.getVectorSchemaRoot().getVector(0));
+      assertEquals(field, vector.getField());
+      assertValues(vector);
+    }
+  }
+
+  @ParameterizedTest
+  @MethodSource("storageTypes")
+  void testReadUnderlyingType(ArrowType storage) throws IOException {
+    Field field = field(storage, true);
+    byte[] serialized = writeStream(field);
+    ExtensionTypeRegistry.unregister((JsonType) field.getType());
+    try (ArrowStreamReader reader =
+        new ArrowStreamReader(new ByteArrayInputStream(serialized), 
allocator)) {
+      assertTrue(reader.loadNextBatch());
+      FieldVector vector = reader.getVectorSchemaRoot().getVector(0);
+      assertEquals(storage, vector.getField().getType());
+      assertEquals(field.getMetadata(), vector.getField().getMetadata());
+      assertValues(vector);
+    }
+  }
+
+  private static final String JSON = "{\"message\":\"你好, a JSON value longer 
than twelve bytes\"}";
+
+  private byte[] writeStream(Field field) throws IOException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    try (VectorSchemaRoot root =
+            VectorSchemaRoot.create(new 
Schema(Collections.singletonList(field)), allocator);
+        ArrowStreamWriter writer = new ArrowStreamWriter(root, null, out)) {
+      JsonVector vector = (JsonVector) root.getVector(0);
+      setVector(
+          (VariableWidthFieldVector) vector.getUnderlyingVector(),
+          JSON.getBytes(StandardCharsets.UTF_8),
+          null,
+          "null".getBytes(StandardCharsets.UTF_8));
+      root.setRowCount(3);
+      writer.start();
+      writer.writeBatch();
+      writer.end();
+    }
+    return out.toByteArray();
+  }
+
+  private static void assertValues(FieldVector vector) {
+    assertEquals(3, vector.getValueCount());
+    assertEquals(new Text(JSON), vector.getObject(0));
+    assertTrue(vector.isNull(1));
+    assertEquals(new Text("null"), vector.getObject(2));
+  }
+}
diff --git a/vector/src/test/resources/pyarrow_json_schema.arrow 
b/vector/src/test/resources/pyarrow_json_schema.arrow
new file mode 100644
index 000000000..6c44bff64
Binary files /dev/null and 
b/vector/src/test/resources/pyarrow_json_schema.arrow differ

Reply via email to