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 6dbc5d369 GH-946: Add Variant extension type support (#947)
6dbc5d369 is described below

commit 6dbc5d3690168e381f6b70c94638a9907c5489b3
Author: Tamas Mate <[email protected]>
AuthorDate: Tue Feb 17 06:16:03 2026 +0100

    GH-946: Add Variant extension type support (#947)
    
    ### Summary
    
    This PR adds support for the Variant extension type in Arrow Java,
    enabling storage and manipulation of semi-structured variant data with
    metadata and value buffers.
    
    ### Changes
    
    A new `arrow-variant` module introduces the `Variant` class for parsing
    and working with variant data. This module is separated from the core
    vector module to isolate the `parquet-variant` dependency, so users of
    the Arrow vector library don't have to depend on Parquet. This also
    maintains a clean API boundary between Arrow's core functionality and
    variant-specific parsing logic.
    
    The core vector module gains `VariantType` as an extension type along
    with `VariantVector` for storing variant data as metadata/value buffer
    pairs. The implementation includes reader and writer support through
    `VariantReaderImpl`, `VariantWriterImpl`, and
    `NullableVariantHolderReaderImpl`, with corresponding holder classes for
    use in generated code paths.
    
    ### Testing
    
    - Unit tests for `VariantType`, `VariantVector`, and `Variant` parsing
    - Integration tests with `ListVector` and `MapVector`
    - Extension type round-trip tests
    
    Closes #946
---
 arrow-variant/pom.xml                              |  51 ++
 arrow-variant/src/main/java/module-info.java       |  28 +
 .../java/org/apache/arrow/variant/Variant.java     | 217 ++++++
 .../arrow/variant/extension/VariantType.java       |  93 +++
 .../arrow/variant/extension/VariantVector.java     | 348 +++++++++
 .../variant/holders/NullableVariantHolder.java     |  56 ++
 .../arrow/variant/holders/VariantHolder.java       |  56 ++
 .../impl/NullableVariantHolderReaderImpl.java      |  69 ++
 .../arrow/variant/impl/VariantReaderImpl.java      |  73 ++
 .../arrow/variant/impl/VariantWriterImpl.java      | 121 +++
 .../java/org/apache/arrow/variant/TestVariant.java | 439 +++++++++++
 .../extension/TestVariantExtensionType.java        | 249 ++++++
 .../variant/extension/TestVariantInListVector.java | 202 +++++
 .../variant/extension/TestVariantInMapVector.java  | 125 +++
 .../arrow/variant/extension/TestVariantType.java   | 308 ++++++++
 .../arrow/variant/extension/TestVariantVector.java | 844 +++++++++++++++++++++
 bom/pom.xml                                        |   5 +
 pom.xml                                            |   2 +
 .../codegen/templates/AbstractFieldReader.java     |   4 +-
 19 files changed, 3288 insertions(+), 2 deletions(-)

diff --git a/arrow-variant/pom.xml b/arrow-variant/pom.xml
new file mode 100644
index 000000000..3a842178a
--- /dev/null
+++ b/arrow-variant/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.arrow</groupId>
+    <artifactId>arrow-java-root</artifactId>
+    <version>19.0.0-SNAPSHOT</version>
+  </parent>
+  <artifactId>arrow-variant</artifactId>
+  <name>Arrow Variant</name>
+  <description>Arrow Variant type support.</description>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.arrow</groupId>
+      <artifactId>arrow-memory-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.arrow</groupId>
+      <artifactId>arrow-vector</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.parquet</groupId>
+      <artifactId>parquet-variant</artifactId>
+      <version>${dep.parquet.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.arrow</groupId>
+      <artifactId>arrow-memory-unsafe</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/arrow-variant/src/main/java/module-info.java 
b/arrow-variant/src/main/java/module-info.java
new file mode 100644
index 000000000..da9417396
--- /dev/null
+++ b/arrow-variant/src/main/java/module-info.java
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+@SuppressWarnings("requires-automatic")
+module org.apache.arrow.variant {
+  exports org.apache.arrow.variant;
+  exports org.apache.arrow.variant.extension;
+  exports org.apache.arrow.variant.impl;
+  exports org.apache.arrow.variant.holders;
+
+  requires org.apache.arrow.memory.core;
+  requires org.apache.arrow.vector;
+  requires parquet.variant;
+}
diff --git a/arrow-variant/src/main/java/org/apache/arrow/variant/Variant.java 
b/arrow-variant/src/main/java/org/apache/arrow/variant/Variant.java
new file mode 100644
index 000000000..fa05cdd93
--- /dev/null
+++ b/arrow-variant/src/main/java/org/apache/arrow/variant/Variant.java
@@ -0,0 +1,217 @@
+/*
+ * 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.variant;
+
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+import java.util.UUID;
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+
+/**
+ * Wrapper around parquet-variant's Variant implementation.
+ *
+ * <p>This wrapper exists to isolate the parquet-variant dependency from 
Arrow's public API,
+ * allowing the vector module to expose variant functionality without 
requiring users to depend on
+ * parquet-variant directly. It also ensures that nested variant values (from 
arrays and objects)
+ * are consistently wrapped.
+ */
+public class Variant {
+
+  private final org.apache.parquet.variant.Variant delegate;
+
+  /** Creates a Variant from raw metadata and value byte arrays. */
+  public Variant(byte[] metadata, byte[] value) {
+    this.delegate = new org.apache.parquet.variant.Variant(value, metadata);
+  }
+
+  /** Creates a Variant by copying data from ArrowBuf instances. */
+  public Variant(
+      ArrowBuf metadataBuffer,
+      int metadataStart,
+      int metadataEnd,
+      ArrowBuf valueBuffer,
+      int valueStart,
+      int valueEnd) {
+    byte[] metadata = new byte[metadataEnd - metadataStart];
+    byte[] value = new byte[valueEnd - valueStart];
+    metadataBuffer.getBytes(metadataStart, metadata);
+    valueBuffer.getBytes(valueStart, value);
+    this.delegate = new org.apache.parquet.variant.Variant(value, metadata);
+  }
+
+  private Variant(org.apache.parquet.variant.Variant delegate) {
+    this.delegate = delegate;
+  }
+
+  /** Constructs a Variant from a NullableVariantHolder. */
+  public Variant(NullableVariantHolder holder) {
+    this(
+        holder.metadataBuffer,
+        holder.metadataStart,
+        holder.metadataEnd,
+        holder.valueBuffer,
+        holder.valueStart,
+        holder.valueEnd);
+  }
+
+  public ByteBuffer getValueBuffer() {
+    return delegate.getValueBuffer();
+  }
+
+  public ByteBuffer getMetadataBuffer() {
+    return delegate.getMetadataBuffer();
+  }
+
+  public boolean getBoolean() {
+    return delegate.getBoolean();
+  }
+
+  public byte getByte() {
+    return delegate.getByte();
+  }
+
+  public short getShort() {
+    return delegate.getShort();
+  }
+
+  public int getInt() {
+    return delegate.getInt();
+  }
+
+  public long getLong() {
+    return delegate.getLong();
+  }
+
+  public double getDouble() {
+    return delegate.getDouble();
+  }
+
+  public BigDecimal getDecimal() {
+    return delegate.getDecimal();
+  }
+
+  public float getFloat() {
+    return delegate.getFloat();
+  }
+
+  public ByteBuffer getBinary() {
+    return delegate.getBinary();
+  }
+
+  public UUID getUUID() {
+    return delegate.getUUID();
+  }
+
+  public String getString() {
+    return delegate.getString();
+  }
+
+  public Type getType() {
+    return Type.fromParquet(delegate.getType());
+  }
+
+  public int numObjectElements() {
+    return delegate.numObjectElements();
+  }
+
+  public Variant getFieldByKey(String key) {
+    org.apache.parquet.variant.Variant result = delegate.getFieldByKey(key);
+    return result != null ? wrap(result) : null;
+  }
+
+  public ObjectField getFieldAtIndex(int idx) {
+    org.apache.parquet.variant.Variant.ObjectField field = 
delegate.getFieldAtIndex(idx);
+    return new ObjectField(field.key, wrap(field.value));
+  }
+
+  public int numArrayElements() {
+    return delegate.numArrayElements();
+  }
+
+  public Variant getElementAtIndex(int index) {
+    org.apache.parquet.variant.Variant result = 
delegate.getElementAtIndex(index);
+    return result != null ? wrap(result) : null;
+  }
+
+  private static Variant wrap(org.apache.parquet.variant.Variant 
parquetVariant) {
+    return new Variant(parquetVariant);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    Variant variant = (Variant) o;
+    return 
delegate.getMetadataBuffer().equals(variant.delegate.getMetadataBuffer())
+        && delegate.getValueBuffer().equals(variant.delegate.getValueBuffer());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(delegate.getMetadataBuffer(), 
delegate.getValueBuffer());
+  }
+
+  @Override
+  public String toString() {
+    return "Variant{type=" + getType() + '}';
+  }
+
+  public enum Type {
+    OBJECT,
+    ARRAY,
+    NULL,
+    BOOLEAN,
+    BYTE,
+    SHORT,
+    INT,
+    LONG,
+    STRING,
+    DOUBLE,
+    DECIMAL4,
+    DECIMAL8,
+    DECIMAL16,
+    DATE,
+    TIMESTAMP_TZ,
+    TIMESTAMP_NTZ,
+    FLOAT,
+    BINARY,
+    TIME,
+    TIMESTAMP_NANOS_TZ,
+    TIMESTAMP_NANOS_NTZ,
+    UUID;
+
+    static Type fromParquet(org.apache.parquet.variant.Variant.Type 
parquetType) {
+      return Type.valueOf(parquetType.name());
+    }
+  }
+
+  public static final class ObjectField {
+    public final String key;
+    public final Variant value;
+
+    public ObjectField(String key, Variant value) {
+      this.key = key;
+      this.value = value;
+    }
+  }
+}
diff --git 
a/arrow-variant/src/main/java/org/apache/arrow/variant/extension/VariantType.java
 
b/arrow-variant/src/main/java/org/apache/arrow/variant/extension/VariantType.java
new file mode 100644
index 000000000..3deb70cdc
--- /dev/null
+++ 
b/arrow-variant/src/main/java/org/apache/arrow/variant/extension/VariantType.java
@@ -0,0 +1,93 @@
+/*
+ * 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.variant.extension;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.variant.impl.VariantWriterImpl;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.ValueVector;
+import org.apache.arrow.vector.complex.writer.FieldWriter;
+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.FieldType;
+
+/**
+ * Arrow extension type for <a
+ * 
href="https://github.com/apache/parquet-format/blob/master/VariantEncoding.md";>Parquet
+ * Variant</a> binary encoding. The type itself does not support shredded 
variant data.
+ */
+public final class VariantType extends ExtensionType {
+
+  public static final VariantType INSTANCE = new VariantType();
+
+  public static final String EXTENSION_NAME = "parquet.variant";
+
+  static {
+    ExtensionTypeRegistry.register(INSTANCE);
+  }
+
+  private VariantType() {}
+
+  @Override
+  public ArrowType storageType() {
+    return ArrowType.Struct.INSTANCE;
+  }
+
+  @Override
+  public String extensionName() {
+    return EXTENSION_NAME;
+  }
+
+  @Override
+  public boolean extensionEquals(ExtensionType other) {
+    return other instanceof VariantType;
+  }
+
+  @Override
+  public String serialize() {
+    return "";
+  }
+
+  @Override
+  public ArrowType deserialize(ArrowType storageType, String serializedData) {
+    if (!storageType.equals(this.storageType())) {
+      throw new UnsupportedOperationException(
+          "Cannot construct VariantType from underlying type " + storageType);
+    }
+    return INSTANCE;
+  }
+
+  @Override
+  public FieldVector getNewVector(String name, FieldType fieldType, 
BufferAllocator allocator) {
+    return new VariantVector(name, allocator);
+  }
+
+  @Override
+  public boolean isComplex() {
+    // The type itself is not complex meaning we need separate functions to 
convert/extract
+    // different types.
+    // Meanwhile, the containing vector is complex in terms of containing 
multiple values (metadata
+    // and value)
+    return false;
+  }
+
+  @Override
+  public FieldWriter getNewFieldWriter(ValueVector vector) {
+    return new VariantWriterImpl((VariantVector) vector);
+  }
+}
diff --git 
a/arrow-variant/src/main/java/org/apache/arrow/variant/extension/VariantVector.java
 
b/arrow-variant/src/main/java/org/apache/arrow/variant/extension/VariantVector.java
new file mode 100644
index 000000000..1bbf1a6bd
--- /dev/null
+++ 
b/arrow-variant/src/main/java/org/apache/arrow/variant/extension/VariantVector.java
@@ -0,0 +1,348 @@
+/*
+ * 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.variant.extension;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.util.hash.ArrowBufHasher;
+import org.apache.arrow.variant.Variant;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.variant.holders.VariantHolder;
+import org.apache.arrow.vector.BitVectorHelper;
+import org.apache.arrow.vector.ExtensionTypeVector;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.ValueVector;
+import org.apache.arrow.vector.VarBinaryVector;
+import org.apache.arrow.vector.complex.AbstractStructVector;
+import org.apache.arrow.vector.complex.StructVector;
+import org.apache.arrow.vector.complex.reader.FieldReader;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.ArrowType.Binary;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.FieldType;
+import org.apache.arrow.vector.util.CallBack;
+import org.apache.arrow.vector.util.TransferPair;
+
+/**
+ * Arrow vector for storing {@link VariantType} values.
+ *
+ * <p>Stores semi-structured data (like JSON) as metadata + value binary 
pairs, allowing
+ * type-flexible columnar storage within Arrow's type system.
+ */
+public class VariantVector extends ExtensionTypeVector<StructVector> {
+
+  public static final String METADATA_VECTOR_NAME = "metadata";
+  public static final String VALUE_VECTOR_NAME = "value";
+
+  private final Field rootField;
+
+  /**
+   * Constructs a new VariantVector with the given name and allocator.
+   *
+   * @param name the name of the vector
+   * @param allocator the buffer allocator for memory management
+   */
+  public VariantVector(String name, BufferAllocator allocator) {
+    super(
+        name,
+        allocator,
+        new StructVector(
+            name,
+            allocator,
+            FieldType.nullable(ArrowType.Struct.INSTANCE),
+            null,
+            AbstractStructVector.ConflictPolicy.CONFLICT_ERROR,
+            false));
+    rootField = createVariantField(name);
+    ((FieldVector) this.getUnderlyingVector())
+        .initializeChildrenFromFields(rootField.getChildren());
+  }
+
+  /**
+   * Creates a new VariantVector with the given name. The Variant Field schema 
has to be the same
+   * everywhere, otherwise ArrowBuffer loading might fail during 
serialization/deserialization and
+   * schema mismatches can occur. This includes CompleteType's VARIANT and 
VARIANT_REQUIRED types.
+   */
+  public static Field createVariantField(String name) {
+    return new Field(
+        name, new FieldType(true, VariantType.INSTANCE, null), 
createVariantChildFields());
+  }
+
+  /**
+   * Creates the child fields for the VariantVector. Metadata vector will be 
index 0 and value
+   * vector will be index 1.
+   */
+  public static List<Field> createVariantChildFields() {
+    return List.of(
+        new Field(METADATA_VECTOR_NAME, new FieldType(false, Binary.INSTANCE, 
null), null),
+        new Field(VALUE_VECTOR_NAME, new FieldType(false, Binary.INSTANCE, 
null), null));
+  }
+
+  @Override
+  public void initializeChildrenFromFields(List<Field> children) {
+    // No-op, as children are initialized in the constructor
+  }
+
+  @Override
+  public Field getField() {
+    return rootField;
+  }
+
+  public VarBinaryVector getMetadataVector() {
+    return getUnderlyingVector().getChild(METADATA_VECTOR_NAME, 
VarBinaryVector.class);
+  }
+
+  public VarBinaryVector getValueVector() {
+    return getUnderlyingVector().getChild(VALUE_VECTOR_NAME, 
VarBinaryVector.class);
+  }
+
+  @Override
+  public TransferPair makeTransferPair(ValueVector target) {
+    return new VariantTransferPair(this, (VariantVector) target);
+  }
+
+  @Override
+  public TransferPair getTransferPair(Field field, BufferAllocator allocator) {
+    return new VariantTransferPair(this, new VariantVector(field.getName(), 
allocator));
+  }
+
+  @Override
+  public TransferPair getTransferPair(Field field, BufferAllocator allocator, 
CallBack callBack) {
+    return getTransferPair(field, allocator);
+  }
+
+  @Override
+  public TransferPair getTransferPair(String ref, BufferAllocator allocator) {
+    return new VariantTransferPair(this, new VariantVector(ref, allocator));
+  }
+
+  @Override
+  public TransferPair getTransferPair(String ref, BufferAllocator allocator, 
CallBack callBack) {
+    return getTransferPair(ref, allocator);
+  }
+
+  @Override
+  public TransferPair getTransferPair(BufferAllocator allocator) {
+    return getTransferPair(this.getField().getName(), allocator);
+  }
+
+  @Override
+  public void copyFrom(int fromIndex, int thisIndex, ValueVector from) {
+    getUnderlyingVector()
+        .copyFrom(fromIndex, thisIndex, ((VariantVector) 
from).getUnderlyingVector());
+  }
+
+  @Override
+  public void copyFromSafe(int fromIndex, int thisIndex, ValueVector from) {
+    getUnderlyingVector()
+        .copyFromSafe(fromIndex, thisIndex, ((VariantVector) 
from).getUnderlyingVector());
+  }
+
+  @Override
+  public Object getObject(int index) {
+    if (isNull(index)) {
+      return null;
+    }
+    VarBinaryVector metadataVector = getMetadataVector();
+    VarBinaryVector valueVector = getValueVector();
+
+    int metadataStart = metadataVector.getStartOffset(index);
+    int metadataEnd = metadataVector.getEndOffset(index);
+    int valueStart = valueVector.getStartOffset(index);
+    int valueEnd = valueVector.getEndOffset(index);
+
+    return new Variant(
+        metadataVector.getDataBuffer(),
+        metadataStart,
+        metadataEnd,
+        valueVector.getDataBuffer(),
+        valueStart,
+        valueEnd);
+  }
+
+  /**
+   * Retrieves the variant value at the specified index into the provided 
holder.
+   *
+   * @param index the index of the value to retrieve
+   * @param holder the holder to populate with the variant data
+   */
+  public void get(int index, NullableVariantHolder holder) {
+    if (isNull(index)) {
+      holder.isSet = 0;
+    } else {
+      holder.isSet = 1;
+      VarBinaryVector metadataVector = getMetadataVector();
+      VarBinaryVector valueVector = getValueVector();
+      assert !metadataVector.isNull(index) && !valueVector.isNull(index);
+
+      holder.metadataStart = metadataVector.getStartOffset(index);
+      holder.metadataEnd = metadataVector.getEndOffset(index);
+      holder.metadataBuffer = metadataVector.getDataBuffer();
+      holder.valueStart = valueVector.getStartOffset(index);
+      holder.valueEnd = valueVector.getEndOffset(index);
+      holder.valueBuffer = valueVector.getDataBuffer();
+    }
+  }
+
+  /**
+   * Retrieves the variant value at the specified index into the provided 
non-nullable holder.
+   *
+   * @param index the index of the value to retrieve
+   * @param holder the holder to populate with the variant data
+   */
+  public void get(int index, VariantHolder holder) {
+    VarBinaryVector metadataVector = getMetadataVector();
+    VarBinaryVector valueVector = getValueVector();
+    assert !metadataVector.isNull(index) && !valueVector.isNull(index);
+
+    holder.metadataStart = metadataVector.getStartOffset(index);
+    holder.metadataEnd = metadataVector.getEndOffset(index);
+    holder.metadataBuffer = metadataVector.getDataBuffer();
+    holder.valueStart = valueVector.getStartOffset(index);
+    holder.valueEnd = valueVector.getEndOffset(index);
+    holder.valueBuffer = valueVector.getDataBuffer();
+  }
+
+  /**
+   * Sets the variant value at the specified index from the provided holder.
+   *
+   * @param index the index at which to set the value
+   * @param holder the holder containing the variant data to set
+   */
+  public void set(int index, VariantHolder holder) {
+    BitVectorHelper.setBit(getUnderlyingVector().getValidityBuffer(), index);
+    getMetadataVector()
+        .set(index, 1, holder.metadataStart, holder.metadataEnd, 
holder.metadataBuffer);
+    getValueVector().set(index, 1, holder.valueStart, holder.valueEnd, 
holder.valueBuffer);
+  }
+
+  /**
+   * Sets the variant value at the specified index from the provided nullable 
holder.
+   *
+   * @param index the index at which to set the value
+   * @param holder the nullable holder containing the variant data to set
+   */
+  public void set(int index, NullableVariantHolder holder) {
+    BitVectorHelper.setValidityBit(getUnderlyingVector().getValidityBuffer(), 
index, holder.isSet);
+    if (holder.isSet == 0) {
+      return;
+    }
+    getMetadataVector()
+        .set(index, 1, holder.metadataStart, holder.metadataEnd, 
holder.metadataBuffer);
+    getValueVector().set(index, 1, holder.valueStart, holder.valueEnd, 
holder.valueBuffer);
+  }
+
+  /**
+   * Sets the variant value at the specified index from the provided holder, 
with bounds checking.
+   *
+   * @param index the index at which to set the value
+   * @param holder the holder containing the variant data to set
+   */
+  public void setSafe(int index, VariantHolder holder) {
+    getUnderlyingVector().setIndexDefined(index);
+    getMetadataVector()
+        .setSafe(index, 1, holder.metadataStart, holder.metadataEnd, 
holder.metadataBuffer);
+    getValueVector().setSafe(index, 1, holder.valueStart, holder.valueEnd, 
holder.valueBuffer);
+  }
+
+  /**
+   * Sets the variant value at the specified index from the provided nullable 
holder, with bounds
+   * checking.
+   *
+   * @param index the index at which to set the value
+   * @param holder the nullable holder containing the variant data to set
+   */
+  public void setSafe(int index, NullableVariantHolder holder) {
+    if (holder.isSet == 0) {
+      getUnderlyingVector().setNull(index);
+      return;
+    }
+    getUnderlyingVector().setIndexDefined(index);
+    getMetadataVector()
+        .setSafe(index, 1, holder.metadataStart, holder.metadataEnd, 
holder.metadataBuffer);
+    getValueVector().setSafe(index, 1, holder.valueStart, holder.valueEnd, 
holder.valueBuffer);
+  }
+
+  /** Sets the value at the given index from the provided Variant. */
+  public void setSafe(int index, Variant variant) {
+    ByteBuffer metadataBuffer = variant.getMetadataBuffer();
+    ByteBuffer valueBuffer = variant.getValueBuffer();
+    int metadataLength = metadataBuffer.remaining();
+    int valueLength = valueBuffer.remaining();
+    try (ArrowBuf metaBuf = getAllocator().buffer(metadataLength);
+        ArrowBuf valBuf = getAllocator().buffer(valueLength)) {
+      metaBuf.setBytes(0, metadataBuffer.duplicate());
+      valBuf.setBytes(0, valueBuffer.duplicate());
+      getUnderlyingVector().setIndexDefined(index);
+      getMetadataVector().setSafe(index, 1, 0, metadataLength, metaBuf);
+      getValueVector().setSafe(index, 1, 0, valueLength, valBuf);
+    }
+  }
+
+  @Override
+  protected FieldReader getReaderImpl() {
+    return new org.apache.arrow.variant.impl.VariantReaderImpl(this);
+  }
+
+  @Override
+  public int hashCode(int index) {
+    return hashCode(index, null);
+  }
+
+  @Override
+  public int hashCode(int index, ArrowBufHasher hasher) {
+    return getUnderlyingVector().hashCode(index, hasher);
+  }
+
+  /**
+   * VariantTransferPair is a transfer pair for VariantVector. It transfers 
the metadata and value
+   * together using the underlyingVector's transfer pair.
+   */
+  protected static class VariantTransferPair implements TransferPair {
+    private final TransferPair pair;
+    private final VariantVector from;
+    private final VariantVector to;
+
+    public VariantTransferPair(VariantVector from, VariantVector to) {
+      this.from = from;
+      this.to = to;
+      this.pair = 
from.getUnderlyingVector().makeTransferPair((to).getUnderlyingVector());
+    }
+
+    @Override
+    public void transfer() {
+      pair.transfer();
+    }
+
+    @Override
+    public void splitAndTransfer(int startIndex, int length) {
+      pair.splitAndTransfer(startIndex, length);
+    }
+
+    @Override
+    public ValueVector getTo() {
+      return to;
+    }
+
+    @Override
+    public void copyValueSafe(int from, int to) {
+      pair.copyValueSafe(from, to);
+    }
+  }
+}
diff --git 
a/arrow-variant/src/main/java/org/apache/arrow/variant/holders/NullableVariantHolder.java
 
b/arrow-variant/src/main/java/org/apache/arrow/variant/holders/NullableVariantHolder.java
new file mode 100644
index 000000000..b78d4a201
--- /dev/null
+++ 
b/arrow-variant/src/main/java/org/apache/arrow/variant/holders/NullableVariantHolder.java
@@ -0,0 +1,56 @@
+/*
+ * 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.variant.holders;
+
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.variant.extension.VariantType;
+import org.apache.arrow.vector.holders.ExtensionHolder;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+
+@SuppressWarnings("checkstyle:VisibilityModifier")
+public final class NullableVariantHolder extends ExtensionHolder {
+
+  public int isSet;
+  public int metadataStart;
+  public int metadataEnd;
+  public ArrowBuf metadataBuffer;
+  public int valueStart;
+  public int valueEnd;
+  public ArrowBuf valueBuffer;
+
+  public NullableVariantHolder() {}
+
+  @Override
+  public boolean equals(Object obj) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public int hashCode() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String toString() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public ArrowType type() {
+    return VariantType.INSTANCE;
+  }
+}
diff --git 
a/arrow-variant/src/main/java/org/apache/arrow/variant/holders/VariantHolder.java
 
b/arrow-variant/src/main/java/org/apache/arrow/variant/holders/VariantHolder.java
new file mode 100644
index 000000000..e3947ac43
--- /dev/null
+++ 
b/arrow-variant/src/main/java/org/apache/arrow/variant/holders/VariantHolder.java
@@ -0,0 +1,56 @@
+/*
+ * 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.variant.holders;
+
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.variant.extension.VariantType;
+import org.apache.arrow.vector.holders.ExtensionHolder;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+
+@SuppressWarnings("checkstyle:VisibilityModifier")
+public final class VariantHolder extends ExtensionHolder {
+
+  public final int isSet = 1;
+  public int metadataStart;
+  public int metadataEnd;
+  public ArrowBuf metadataBuffer;
+  public int valueStart;
+  public int valueEnd;
+  public ArrowBuf valueBuffer;
+
+  public VariantHolder() {}
+
+  @Override
+  public boolean equals(Object obj) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public int hashCode() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public String toString() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public ArrowType type() {
+    return VariantType.INSTANCE;
+  }
+}
diff --git 
a/arrow-variant/src/main/java/org/apache/arrow/variant/impl/NullableVariantHolderReaderImpl.java
 
b/arrow-variant/src/main/java/org/apache/arrow/variant/impl/NullableVariantHolderReaderImpl.java
new file mode 100644
index 000000000..1645529c0
--- /dev/null
+++ 
b/arrow-variant/src/main/java/org/apache/arrow/variant/impl/NullableVariantHolderReaderImpl.java
@@ -0,0 +1,69 @@
+/*
+ * 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.variant.impl;
+
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.vector.complex.impl.AbstractFieldReader;
+import org.apache.arrow.vector.types.Types;
+
+public class NullableVariantHolderReaderImpl extends AbstractFieldReader {
+  private final NullableVariantHolder holder;
+
+  public NullableVariantHolderReaderImpl(NullableVariantHolder holder) {
+    this.holder = holder;
+  }
+
+  @Override
+  public int size() {
+    throw new UnsupportedOperationException("You can't call size on a Holder 
value reader.");
+  }
+
+  @Override
+  public boolean next() {
+    throw new UnsupportedOperationException("You can't call next on a single 
value reader.");
+  }
+
+  @Override
+  public void setPosition(int index) {
+    throw new UnsupportedOperationException("You can't call setPosition on a 
single value reader.");
+  }
+
+  @Override
+  public Types.MinorType getMinorType() {
+    return Types.MinorType.EXTENSIONTYPE;
+  }
+
+  @Override
+  public boolean isSet() {
+    return holder.isSet == 1;
+  }
+
+  /**
+   * Reads the variant holder data into the provided holder.
+   *
+   * @param h the holder to read into
+   */
+  public void read(NullableVariantHolder h) {
+    h.metadataStart = this.holder.metadataStart;
+    h.metadataEnd = this.holder.metadataEnd;
+    h.metadataBuffer = this.holder.metadataBuffer;
+    h.valueStart = this.holder.valueStart;
+    h.valueEnd = this.holder.valueEnd;
+    h.valueBuffer = this.holder.valueBuffer;
+    h.isSet = this.isSet() ? 1 : 0;
+  }
+}
diff --git 
a/arrow-variant/src/main/java/org/apache/arrow/variant/impl/VariantReaderImpl.java
 
b/arrow-variant/src/main/java/org/apache/arrow/variant/impl/VariantReaderImpl.java
new file mode 100644
index 000000000..670104b7d
--- /dev/null
+++ 
b/arrow-variant/src/main/java/org/apache/arrow/variant/impl/VariantReaderImpl.java
@@ -0,0 +1,73 @@
+/*
+ * 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.variant.impl;
+
+import org.apache.arrow.variant.extension.VariantVector;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.variant.holders.VariantHolder;
+import org.apache.arrow.vector.complex.impl.AbstractFieldReader;
+import org.apache.arrow.vector.holders.ExtensionHolder;
+import org.apache.arrow.vector.types.Types;
+import org.apache.arrow.vector.types.pojo.Field;
+
+public class VariantReaderImpl extends AbstractFieldReader {
+  private final VariantVector vector;
+
+  public VariantReaderImpl(VariantVector vector) {
+    this.vector = vector;
+  }
+
+  @Override
+  public Types.MinorType getMinorType() {
+    return this.vector.getMinorType();
+  }
+
+  @Override
+  public Field getField() {
+    return this.vector.getField();
+  }
+
+  @Override
+  public boolean isSet() {
+    return !this.vector.isNull(this.idx());
+  }
+
+  @Override
+  public void read(ExtensionHolder holder) {
+    if (holder instanceof VariantHolder) {
+      vector.get(idx(), (VariantHolder) holder);
+    } else if (holder instanceof NullableVariantHolder) {
+      vector.get(idx(), (NullableVariantHolder) holder);
+    } else {
+      throw new IllegalArgumentException(
+          "Unsupported holder type for VariantReader: " + holder.getClass());
+    }
+  }
+
+  public void read(VariantHolder h) {
+    this.vector.get(this.idx(), h);
+  }
+
+  public void read(NullableVariantHolder h) {
+    this.vector.get(this.idx(), h);
+  }
+
+  @Override
+  public Object readObject() {
+    return this.vector.getObject(this.idx());
+  }
+}
diff --git 
a/arrow-variant/src/main/java/org/apache/arrow/variant/impl/VariantWriterImpl.java
 
b/arrow-variant/src/main/java/org/apache/arrow/variant/impl/VariantWriterImpl.java
new file mode 100644
index 000000000..266ddb75d
--- /dev/null
+++ 
b/arrow-variant/src/main/java/org/apache/arrow/variant/impl/VariantWriterImpl.java
@@ -0,0 +1,121 @@
+/*
+ * 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.variant.impl;
+
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.variant.Variant;
+import org.apache.arrow.variant.extension.VariantVector;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.variant.holders.VariantHolder;
+import org.apache.arrow.vector.complex.impl.AbstractExtensionTypeWriter;
+import org.apache.arrow.vector.holders.ExtensionHolder;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+
+/**
+ * Writer implementation for VARIANT extension type vectors.
+ *
+ * <p>This writer handles writing variant data to a {@link VariantVector}. It 
accepts both {@link
+ * VariantHolder} and {@link NullableVariantHolder} objects containing 
metadata and value buffers
+ * and writes them to the appropriate position in the vector.
+ */
+public class VariantWriterImpl extends 
AbstractExtensionTypeWriter<VariantVector> {
+
+  private static final String UNSUPPORTED_TYPE_TEMPLATE = "Unsupported type 
for Variant: %s";
+
+  /**
+   * Constructs a new VariantWriterImpl for the given vector.
+   *
+   * @param vector the variant vector to write to
+   */
+  public VariantWriterImpl(VariantVector vector) {
+    super(vector);
+  }
+
+  /**
+   * Writes an extension type or variant value to the vector.
+   *
+   * <p>This method handles {@link ExtensionHolder} by delegating to {@link 
#write(ExtensionHolder)}
+   * and {@link Variant} by delegating to {@link #writeVariant(Variant)}.
+   *
+   * @param object the object to write, must be an {@link ExtensionHolder} or 
{@link Variant}
+   * @throws IllegalArgumentException if the object is not an {@link 
ExtensionHolder} or {@link
+   *     Variant}
+   */
+  @Override
+  public void writeExtension(Object object) {
+    if (object instanceof ExtensionHolder) {
+      write((ExtensionHolder) object);
+    } else if (object instanceof Variant) {
+      writeVariant((Variant) object);
+    } else {
+      throw new IllegalArgumentException(
+          String.format(UNSUPPORTED_TYPE_TEMPLATE, 
object.getClass().getName()));
+    }
+  }
+
+  private void writeVariant(Variant variant) {
+    java.nio.ByteBuffer metadataBuffer = variant.getMetadataBuffer();
+    java.nio.ByteBuffer valueBuffer = variant.getValueBuffer();
+    int metadataLength = metadataBuffer.remaining();
+    int valueLength = valueBuffer.remaining();
+    try (ArrowBuf metadataBuf = vector.getAllocator().buffer(metadataLength);
+        ArrowBuf valueBuf = vector.getAllocator().buffer(valueLength)) {
+      metadataBuf.setBytes(0, metadataBuffer.duplicate());
+      valueBuf.setBytes(0, valueBuffer.duplicate());
+      NullableVariantHolder holder = new NullableVariantHolder();
+      holder.isSet = 1;
+      holder.metadataBuffer = metadataBuf;
+      holder.metadataStart = 0;
+      holder.metadataEnd = metadataLength;
+      holder.valueBuffer = valueBuf;
+      holder.valueStart = 0;
+      holder.valueEnd = valueLength;
+      vector.setSafe(getPosition(), holder);
+      vector.setValueCount(getPosition() + 1);
+    }
+  }
+
+  @Override
+  public void writeExtension(Object value, ArrowType type) {
+    writeExtension(value);
+  }
+
+  /**
+   * Writes a variant holder to the vector at the current position.
+   *
+   * <p>The holder can be either a {@link VariantHolder} (non-nullable, always 
set) or a {@link
+   * NullableVariantHolder} (nullable, may be null). The data is written using 
{@link
+   * VariantVector#setSafe(int, NullableVariantHolder)} which handles buffer 
allocation and copying.
+   *
+   * @param extensionHolder the variant holder to write, must be a {@link 
VariantHolder} or {@link
+   *     NullableVariantHolder}
+   * @throws IllegalArgumentException if the holder is neither a {@link 
VariantHolder} nor a {@link
+   *     NullableVariantHolder}
+   */
+  @Override
+  public void write(ExtensionHolder extensionHolder) {
+    if (extensionHolder instanceof VariantHolder) {
+      vector.setSafe(getPosition(), (VariantHolder) extensionHolder);
+    } else if (extensionHolder instanceof NullableVariantHolder) {
+      vector.setSafe(getPosition(), (NullableVariantHolder) extensionHolder);
+    } else {
+      throw new IllegalArgumentException(
+          String.format(UNSUPPORTED_TYPE_TEMPLATE, 
extensionHolder.getClass().getName()));
+    }
+    vector.setValueCount(getPosition() + 1);
+  }
+}
diff --git 
a/arrow-variant/src/test/java/org/apache/arrow/variant/TestVariant.java 
b/arrow-variant/src/test/java/org/apache/arrow/variant/TestVariant.java
new file mode 100644
index 000000000..bc46a6861
--- /dev/null
+++ b/arrow-variant/src/test/java/org/apache/arrow/variant/TestVariant.java
@@ -0,0 +1,439 @@
+/*
+ * 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.variant;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.util.UUID;
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.parquet.variant.VariantBuilder;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class TestVariant {
+
+  private BufferAllocator allocator;
+
+  @BeforeEach
+  void beforeEach() {
+    allocator = new RootAllocator();
+  }
+
+  @AfterEach
+  void afterEach() {
+    allocator.close();
+  }
+
+  static Variant buildVariant(VariantBuilder builder) {
+    org.apache.parquet.variant.Variant parquetVariant = builder.build();
+    ByteBuffer valueBuf = parquetVariant.getValueBuffer();
+    ByteBuffer metaBuf = parquetVariant.getMetadataBuffer();
+    byte[] valueBytes = new byte[valueBuf.remaining()];
+    byte[] metaBytes = new byte[metaBuf.remaining()];
+    valueBuf.get(valueBytes);
+    metaBuf.get(metaBytes);
+    return new Variant(metaBytes, valueBytes);
+  }
+
+  public static Variant variantString(String value) {
+    VariantBuilder builder = new VariantBuilder();
+    builder.appendString(value);
+    return buildVariant(builder);
+  }
+
+  @Test
+  void testConstructionWithArrowBuf() {
+    VariantBuilder builder = new VariantBuilder();
+    builder.appendInt(42);
+    Variant source = buildVariant(builder);
+    int metaLen = source.getMetadataBuffer().remaining();
+    int valueLen = source.getValueBuffer().remaining();
+
+    try (ArrowBuf metadataArrowBuf = allocator.buffer(metaLen + 2);
+        ArrowBuf valueArrowBuf = allocator.buffer(valueLen + 3)) {
+      metadataArrowBuf.setBytes(2, source.getMetadataBuffer());
+      valueArrowBuf.setBytes(3, source.getValueBuffer());
+
+      Variant variant =
+          new Variant(metadataArrowBuf, 2, 2 + metaLen, valueArrowBuf, 3, 3 + 
valueLen);
+
+      assertEquals(Variant.Type.INT, variant.getType());
+      assertEquals(42, variant.getInt());
+    }
+  }
+
+  @Test
+  void testNullType() {
+    VariantBuilder builder = new VariantBuilder();
+    builder.appendNull();
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.NULL, variant.getType());
+  }
+
+  @Test
+  void testBooleanType() {
+    VariantBuilder builder = new VariantBuilder();
+    builder.appendBoolean(true);
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.BOOLEAN, variant.getType());
+    assertTrue(variant.getBoolean());
+
+    builder = new VariantBuilder();
+    builder.appendBoolean(false);
+    variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.BOOLEAN, variant.getType());
+    assertFalse(variant.getBoolean());
+  }
+
+  @Test
+  void testByteType() {
+    VariantBuilder builder = new VariantBuilder();
+    builder.appendByte((byte) 42);
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.BYTE, variant.getType());
+    assertEquals((byte) 42, variant.getByte());
+  }
+
+  @Test
+  void testShortType() {
+    VariantBuilder builder = new VariantBuilder();
+    builder.appendShort((short) 1234);
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.SHORT, variant.getType());
+    assertEquals((short) 1234, variant.getShort());
+  }
+
+  @Test
+  void testIntType() {
+    VariantBuilder builder = new VariantBuilder();
+    builder.appendInt(123456);
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.INT, variant.getType());
+    assertEquals(123456, variant.getInt());
+  }
+
+  @Test
+  void testLongType() {
+    VariantBuilder builder = new VariantBuilder();
+    builder.appendLong(9876543210L);
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.LONG, variant.getType());
+    assertEquals(9876543210L, variant.getLong());
+  }
+
+  @Test
+  void testFloatType() {
+    VariantBuilder builder = new VariantBuilder();
+    builder.appendFloat(3.14f);
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.FLOAT, variant.getType());
+    assertEquals(3.14f, variant.getFloat(), 0.001f);
+  }
+
+  @Test
+  void testDoubleType() {
+    VariantBuilder builder = new VariantBuilder();
+    builder.appendDouble(3.14159265359);
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.DOUBLE, variant.getType());
+    assertEquals(3.14159265359, variant.getDouble(), 0.0000001);
+  }
+
+  @Test
+  void testStringType() {
+    VariantBuilder builder = new VariantBuilder();
+    builder.appendString("hello world");
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.STRING, variant.getType());
+    assertEquals("hello world", variant.getString());
+  }
+
+  @Test
+  void testDecimalType() {
+    VariantBuilder builder = new VariantBuilder();
+    builder.appendDecimal(new BigDecimal("123.456"));
+    Variant variant = buildVariant(builder);
+
+    assertTrue(
+        variant.getType() == Variant.Type.DECIMAL4
+            || variant.getType() == Variant.Type.DECIMAL8
+            || variant.getType() == Variant.Type.DECIMAL16);
+    assertEquals(new BigDecimal("123.456"), variant.getDecimal());
+  }
+
+  @Test
+  void testBinaryType() {
+    VariantBuilder builder = new VariantBuilder();
+    byte[] data = new byte[] {1, 2, 3, 4, 5};
+    builder.appendBinary(ByteBuffer.wrap(data));
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.BINARY, variant.getType());
+    ByteBuffer result = variant.getBinary();
+    byte[] resultBytes = new byte[result.remaining()];
+    result.get(resultBytes);
+    assertArrayEquals(data, resultBytes);
+  }
+
+  @Test
+  void testUuidType() {
+    VariantBuilder builder = new VariantBuilder();
+    UUID uuid = UUID.randomUUID();
+    builder.appendUUID(uuid);
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.UUID, variant.getType());
+    assertEquals(uuid, variant.getUUID());
+  }
+
+  @Test
+  void testDateType() {
+    VariantBuilder builder = new VariantBuilder();
+    int daysSinceEpoch = 19000;
+    builder.appendDate(daysSinceEpoch);
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.DATE, variant.getType());
+  }
+
+  @Test
+  void testTimestampTzType() {
+    VariantBuilder builder = new VariantBuilder();
+    long micros = System.currentTimeMillis() * 1000;
+    builder.appendTimestampTz(micros);
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.TIMESTAMP_TZ, variant.getType());
+  }
+
+  @Test
+  void testTimestampNtzType() {
+    VariantBuilder builder = new VariantBuilder();
+    long micros = System.currentTimeMillis() * 1000;
+    builder.appendTimestampNtz(micros);
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.TIMESTAMP_NTZ, variant.getType());
+  }
+
+  @Test
+  void testTimeType() {
+    VariantBuilder builder = new VariantBuilder();
+    long micros = 12345678L;
+    builder.appendTime(micros);
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.TIME, variant.getType());
+  }
+
+  @Test
+  void testObjectType() {
+    VariantBuilder builder = new VariantBuilder();
+    var objBuilder = builder.startObject();
+    objBuilder.appendKey("name");
+    objBuilder.appendString("test");
+    objBuilder.appendKey("value");
+    objBuilder.appendInt(42);
+    builder.endObject();
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.OBJECT, variant.getType());
+    assertEquals(2, variant.numObjectElements());
+
+    Variant nameField = variant.getFieldByKey("name");
+    assertNotNull(nameField);
+    assertEquals(Variant.Type.STRING, nameField.getType());
+    assertEquals("test", nameField.getString());
+
+    Variant valueField = variant.getFieldByKey("value");
+    assertNotNull(valueField);
+    assertEquals(Variant.Type.INT, valueField.getType());
+    assertEquals(42, valueField.getInt());
+
+    assertNull(variant.getFieldByKey("nonexistent"));
+
+    // Empty object
+    builder = new VariantBuilder();
+    builder.startObject();
+    builder.endObject();
+    Variant emptyObj = buildVariant(builder);
+    assertEquals(Variant.Type.OBJECT, emptyObj.getType());
+    assertEquals(0, emptyObj.numObjectElements());
+  }
+
+  @Test
+  void testObjectFieldAtIndex() {
+    VariantBuilder builder = new VariantBuilder();
+    var objBuilder = builder.startObject();
+    objBuilder.appendKey("alpha");
+    objBuilder.appendInt(1);
+    objBuilder.appendKey("beta");
+    objBuilder.appendInt(2);
+    builder.endObject();
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.OBJECT, variant.getType());
+    assertEquals(2, variant.numObjectElements());
+
+    Variant.ObjectField field0 = variant.getFieldAtIndex(0);
+    assertNotNull(field0);
+    assertNotNull(field0.key);
+    assertNotNull(field0.value);
+
+    Variant.ObjectField field1 = variant.getFieldAtIndex(1);
+    assertNotNull(field1);
+    assertNotNull(field1.key);
+    assertNotNull(field1.value);
+  }
+
+  @Test
+  void testArrayType() {
+    VariantBuilder builder = new VariantBuilder();
+    var arrayBuilder = builder.startArray();
+    arrayBuilder.appendInt(1);
+    arrayBuilder.appendInt(2);
+    arrayBuilder.appendInt(3);
+    builder.endArray();
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.ARRAY, variant.getType());
+    assertEquals(3, variant.numArrayElements());
+
+    Variant elem0 = variant.getElementAtIndex(0);
+    assertNotNull(elem0);
+    assertEquals(Variant.Type.INT, elem0.getType());
+    assertEquals(1, elem0.getInt());
+
+    Variant elem1 = variant.getElementAtIndex(1);
+    assertEquals(2, elem1.getInt());
+
+    Variant elem2 = variant.getElementAtIndex(2);
+    assertEquals(3, elem2.getInt());
+
+    assertNull(variant.getElementAtIndex(-1));
+    assertNull(variant.getElementAtIndex(3));
+
+    // Empty array
+    builder = new VariantBuilder();
+    builder.startArray();
+    builder.endArray();
+    Variant emptyArr = buildVariant(builder);
+    assertEquals(Variant.Type.ARRAY, emptyArr.getType());
+    assertEquals(0, emptyArr.numArrayElements());
+  }
+
+  @Test
+  void testNestedStructure() {
+    VariantBuilder builder = new VariantBuilder();
+    var objBuilder = builder.startObject();
+    objBuilder.appendKey("items");
+    var arrayBuilder = objBuilder.startArray();
+    arrayBuilder.appendString("a");
+    arrayBuilder.appendString("b");
+    objBuilder.endArray();
+    builder.endObject();
+    Variant variant = buildVariant(builder);
+
+    assertEquals(Variant.Type.OBJECT, variant.getType());
+    Variant items = variant.getFieldByKey("items");
+    assertNotNull(items);
+    assertEquals(Variant.Type.ARRAY, items.getType());
+    assertEquals(2, items.numArrayElements());
+    assertEquals("a", items.getElementAtIndex(0).getString());
+    assertEquals("b", items.getElementAtIndex(1).getString());
+  }
+
+  @Test
+  void testEquals() {
+    VariantBuilder builder1 = new VariantBuilder();
+    builder1.appendString("test");
+    Variant variant1 = buildVariant(builder1);
+
+    VariantBuilder builder2 = new VariantBuilder();
+    builder2.appendString("test");
+    Variant variant2 = buildVariant(builder2);
+
+    VariantBuilder builder3 = new VariantBuilder();
+    builder3.appendString("different");
+    Variant variant3 = buildVariant(builder3);
+
+    assertEquals(variant1, variant1);
+    assertEquals(variant1, variant2);
+    assertNotEquals(variant1, variant3);
+    assertNotEquals(variant1, null);
+    assertNotEquals(variant1, "not a variant");
+  }
+
+  @Test
+  void testHashCode() {
+    VariantBuilder builder1 = new VariantBuilder();
+    builder1.appendInt(42);
+    Variant variant1 = buildVariant(builder1);
+
+    VariantBuilder builder2 = new VariantBuilder();
+    builder2.appendInt(42);
+    Variant variant2 = buildVariant(builder2);
+
+    assertEquals(variant1.hashCode(), variant2.hashCode());
+  }
+
+  @Test
+  void testToString() {
+    VariantBuilder builder = new VariantBuilder();
+    builder.appendString("test");
+    Variant variant = buildVariant(builder);
+
+    String str = variant.toString();
+    assertNotNull(str);
+    assertTrue(str.contains("type="));
+  }
+
+  @Test
+  void testTypeEnumsMatch() {
+    for (Variant.Type arrowType : Variant.Type.values()) {
+      org.apache.parquet.variant.Variant.Type parquetType =
+          org.apache.parquet.variant.Variant.Type.valueOf(arrowType.name());
+      assertEquals(arrowType, Variant.Type.fromParquet(parquetType));
+    }
+    for (org.apache.parquet.variant.Variant.Type parquetType :
+        org.apache.parquet.variant.Variant.Type.values()) {
+      Variant.Type arrowType = Variant.Type.valueOf(parquetType.name());
+      assertEquals(parquetType.name(), arrowType.name());
+    }
+  }
+}
diff --git 
a/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantExtensionType.java
 
b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantExtensionType.java
new file mode 100644
index 000000000..f3213d523
--- /dev/null
+++ 
b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantExtensionType.java
@@ -0,0 +1,249 @@
+/*
+ * 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.variant.extension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.Collections;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.variant.TestVariant;
+import org.apache.arrow.variant.Variant;
+import org.apache.arrow.vector.ExtensionTypeVector;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.VarBinaryVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.compare.Range;
+import org.apache.arrow.vector.compare.RangeEqualsVisitor;
+import org.apache.arrow.vector.complex.StructVector;
+import org.apache.arrow.vector.complex.writer.BaseWriter;
+import org.apache.arrow.vector.ipc.ArrowFileReader;
+import org.apache.arrow.vector.ipc.ArrowFileWriter;
+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;
+import org.apache.arrow.vector.util.VectorBatchAppender;
+import org.apache.arrow.vector.validate.ValidateVectorVisitor;
+import org.junit.jupiter.api.Test;
+
+public class TestVariantExtensionType {
+
+  private static void ensureRegistered(ArrowType.ExtensionType type) {
+    if (ExtensionTypeRegistry.lookup(type.extensionName()) == null) {
+      ExtensionTypeRegistry.register(type);
+    }
+  }
+
+  @Test
+  public void roundtripVariant() throws IOException {
+    ensureRegistered(VariantType.INSTANCE);
+    final Schema schema =
+        new Schema(Collections.singletonList(Field.nullable("a", 
VariantType.INSTANCE)));
+    try (final BufferAllocator allocator = new 
RootAllocator(Integer.MAX_VALUE);
+        final VectorSchemaRoot root = VectorSchemaRoot.create(schema, 
allocator)) {
+      VariantVector vector = (VariantVector) root.getVector("a");
+      vector.allocateNew();
+
+      vector.setSafe(0, TestVariant.variantString("hello"));
+      vector.setSafe(1, TestVariant.variantString("world"));
+      vector.setValueCount(2);
+      root.setRowCount(2);
+
+      final File file = File.createTempFile("varianttest", ".arrow");
+      try (final WritableByteChannel channel =
+              FileChannel.open(Paths.get(file.getAbsolutePath()), 
StandardOpenOption.WRITE);
+          final ArrowFileWriter writer = new ArrowFileWriter(root, null, 
channel)) {
+        writer.start();
+        writer.writeBatch();
+        writer.end();
+      }
+
+      try (final SeekableByteChannel channel =
+              Files.newByteChannel(Paths.get(file.getAbsolutePath()));
+          final ArrowFileReader reader = new ArrowFileReader(channel, 
allocator)) {
+        reader.loadNextBatch();
+        final VectorSchemaRoot readerRoot = reader.getVectorSchemaRoot();
+        assertEquals(root.getSchema(), readerRoot.getSchema());
+
+        final Field field = readerRoot.getSchema().getFields().get(0);
+        final VariantType expectedType = VariantType.INSTANCE;
+        assertEquals(
+            field.getMetadata().get(ExtensionType.EXTENSION_METADATA_KEY_NAME),
+            expectedType.extensionName());
+        assertEquals(
+            
field.getMetadata().get(ExtensionType.EXTENSION_METADATA_KEY_METADATA),
+            expectedType.serialize());
+
+        final ExtensionTypeVector deserialized =
+            (ExtensionTypeVector) readerRoot.getFieldVectors().get(0);
+        assertEquals(vector.getValueCount(), deserialized.getValueCount());
+        for (int i = 0; i < vector.getValueCount(); i++) {
+          assertEquals(vector.isNull(i), deserialized.isNull(i));
+          if (!vector.isNull(i)) {
+            assertEquals(vector.getObject(i), deserialized.getObject(i));
+          }
+        }
+      }
+    }
+  }
+
+  @Test
+  public void readVariantAsUnderlyingType() throws IOException {
+    ensureRegistered(VariantType.INSTANCE);
+    final Schema schema =
+        new 
Schema(Collections.singletonList(VariantVector.createVariantField("a")));
+    try (final BufferAllocator allocator = new 
RootAllocator(Integer.MAX_VALUE);
+        final VectorSchemaRoot root = VectorSchemaRoot.create(schema, 
allocator)) {
+      VariantVector vector = (VariantVector) root.getVector("a");
+      vector.allocateNew();
+
+      vector.setSafe(0, TestVariant.variantString("hello"));
+      vector.setValueCount(1);
+      root.setRowCount(1);
+
+      final File file = File.createTempFile("varianttest", ".arrow");
+      try (final WritableByteChannel channel =
+              FileChannel.open(Paths.get(file.getAbsolutePath()), 
StandardOpenOption.WRITE);
+          final ArrowFileWriter writer = new ArrowFileWriter(root, null, 
channel)) {
+        writer.start();
+        writer.writeBatch();
+        writer.end();
+      }
+
+      ExtensionTypeRegistry.unregister(VariantType.INSTANCE);
+
+      try (final SeekableByteChannel channel =
+              Files.newByteChannel(Paths.get(file.getAbsolutePath()));
+          final ArrowFileReader reader = new ArrowFileReader(channel, 
allocator)) {
+        reader.loadNextBatch();
+        VectorSchemaRoot readRoot = reader.getVectorSchemaRoot();
+
+        // Verify schema properties
+        assertEquals(1, readRoot.getSchema().getFields().size());
+        assertEquals("a", readRoot.getSchema().getFields().get(0).getName());
+        assertTrue(readRoot.getSchema().getFields().get(0).getType() 
instanceof ArrowType.Struct);
+
+        // Verify extension metadata is preserved
+        final Field field = readRoot.getSchema().getFields().get(0);
+        assertEquals(
+            VariantType.EXTENSION_NAME,
+            
field.getMetadata().get(ExtensionType.EXTENSION_METADATA_KEY_NAME));
+        assertEquals("", 
field.getMetadata().get(ExtensionType.EXTENSION_METADATA_KEY_METADATA));
+
+        // Verify vector type and row count
+        assertEquals(1, readRoot.getRowCount());
+        FieldVector readVector = readRoot.getVector("a");
+        assertEquals(StructVector.class, readVector.getClass());
+
+        // Verify value count matches
+        StructVector structVector = (StructVector) readVector;
+        assertEquals(vector.getValueCount(), structVector.getValueCount());
+
+        // Verify the underlying data can be accessed from child vectors
+        VarBinaryVector metadataVector =
+            structVector.getChild(VariantVector.METADATA_VECTOR_NAME, 
VarBinaryVector.class);
+        VarBinaryVector valueVector =
+            structVector.getChild(VariantVector.VALUE_VECTOR_NAME, 
VarBinaryVector.class);
+        assertNotNull(metadataVector);
+        assertNotNull(valueVector);
+        assertEquals(1, metadataVector.getValueCount());
+        assertEquals(1, valueVector.getValueCount());
+      }
+    }
+  }
+
+  @Test
+  public void testVariantVectorCompare() {
+    VariantType variantType = VariantType.INSTANCE;
+    ExtensionTypeRegistry.register(variantType);
+    Variant hello = TestVariant.variantString("hello");
+    Variant world = TestVariant.variantString("world");
+    try (final BufferAllocator allocator = new 
RootAllocator(Integer.MAX_VALUE);
+        VariantVector a1 =
+            (VariantVector)
+                variantType.getNewVector("a", FieldType.nullable(variantType), 
allocator);
+        VariantVector a2 =
+            (VariantVector)
+                variantType.getNewVector("a", FieldType.nullable(variantType), 
allocator);
+        VariantVector bb =
+            (VariantVector)
+                variantType.getNewVector("a", FieldType.nullable(variantType), 
allocator)) {
+
+      ValidateVectorVisitor validateVisitor = new ValidateVectorVisitor();
+      validateVisitor.visit(a1, null);
+
+      a1.allocateNew();
+      a2.allocateNew();
+      bb.allocateNew();
+
+      a1.setSafe(0, hello);
+      a1.setSafe(1, world);
+      a1.setValueCount(2);
+
+      a2.setSafe(0, hello);
+      a2.setSafe(1, world);
+      a2.setValueCount(2);
+
+      bb.setSafe(0, world);
+      bb.setSafe(1, hello);
+      bb.setValueCount(2);
+
+      Range range = new Range(0, 0, a1.getValueCount());
+      RangeEqualsVisitor visitor = new RangeEqualsVisitor(a1, a2);
+      assertTrue(visitor.rangeEquals(range));
+
+      visitor = new RangeEqualsVisitor(a1, bb);
+      assertFalse(visitor.rangeEquals(range));
+
+      VectorBatchAppender.batchAppend(a1, a2, bb);
+      assertEquals(6, a1.getValueCount());
+      validateVisitor.visit(a1, null);
+    }
+  }
+
+  @Test
+  public void testVariantCopyAsValueThrowsException() {
+    ensureRegistered(VariantType.INSTANCE);
+    try (BufferAllocator allocator = new RootAllocator(Integer.MAX_VALUE);
+        VariantVector vector = new VariantVector("variant", allocator)) {
+      vector.allocateNew();
+      vector.setSafe(0, TestVariant.variantString("hello"));
+      vector.setValueCount(1);
+
+      var reader = vector.getReader();
+      reader.setPosition(0);
+
+      assertThrows(
+          IllegalArgumentException.class, () -> 
reader.copyAsValue((BaseWriter.StructWriter) null));
+    }
+  }
+}
diff --git 
a/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantInListVector.java
 
b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantInListVector.java
new file mode 100644
index 000000000..8b6000bc4
--- /dev/null
+++ 
b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantInListVector.java
@@ -0,0 +1,202 @@
+/*
+ * 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.variant.extension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.variant.TestVariant;
+import org.apache.arrow.variant.Variant;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.vector.complex.ListVector;
+import org.apache.arrow.vector.complex.impl.UnionListReader;
+import org.apache.arrow.vector.complex.impl.UnionListWriter;
+import org.apache.arrow.vector.complex.reader.FieldReader;
+import org.apache.arrow.vector.complex.writer.BaseWriter.ExtensionWriter;
+import org.apache.arrow.vector.types.pojo.FieldType;
+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;
+
+public class TestVariantInListVector {
+
+  private BufferAllocator allocator;
+
+  @BeforeEach
+  public void init() {
+    allocator = new RootAllocator(Long.MAX_VALUE);
+  }
+
+  @AfterEach
+  public void terminate() throws Exception {
+    allocator.close();
+  }
+
+  @Test
+  public void testListVectorWithVariantExtensionType() {
+    final FieldType type = FieldType.nullable(VariantType.INSTANCE);
+    try (ListVector inVector = new ListVector("input", allocator, type, null)) 
{
+      Variant variant1 = TestVariant.variantString("hello");
+      Variant variant2 = TestVariant.variantString("bye");
+
+      UnionListWriter writer = inVector.getWriter();
+      writer.allocate();
+
+      writer.setPosition(0);
+      writer.startList();
+      ExtensionWriter extensionWriter = writer.extension(VariantType.INSTANCE);
+      extensionWriter.writeExtension(variant1);
+      extensionWriter.writeExtension(variant2);
+      writer.endList();
+      inVector.setValueCount(1);
+
+      ArrayList<Variant> resultSet = (ArrayList<Variant>) 
inVector.getObject(0);
+      assertEquals(2, resultSet.size());
+      assertEquals(variant1, resultSet.get(0));
+      assertEquals(variant2, resultSet.get(1));
+    }
+  }
+
+  @Test
+  public void testListVectorReaderForVariantExtensionType() {
+    try (ListVector inVector = ListVector.empty("input", allocator)) {
+      Variant variant1 = TestVariant.variantString("hello");
+      Variant variant2 = TestVariant.variantString("bye");
+
+      UnionListWriter writer = inVector.getWriter();
+      writer.allocate();
+
+      writer.setPosition(0);
+      writer.startList();
+      ExtensionWriter extensionWriter = writer.extension(VariantType.INSTANCE);
+      extensionWriter.writeExtension(variant1);
+      writer.endList();
+
+      writer.setPosition(1);
+      writer.startList();
+      extensionWriter.writeExtension(variant2);
+      extensionWriter.writeExtension(variant2);
+      writer.endList();
+
+      inVector.setValueCount(2);
+
+      UnionListReader reader = inVector.getReader();
+      reader.setPosition(0);
+      assertTrue(reader.next());
+      FieldReader variantReader = reader.reader();
+      NullableVariantHolder resultHolder = new NullableVariantHolder();
+      variantReader.read(resultHolder);
+      assertEquals(variant1, new Variant(resultHolder));
+
+      reader.setPosition(1);
+      assertTrue(reader.next());
+      variantReader = reader.reader();
+      variantReader.read(resultHolder);
+      assertEquals(variant2, new Variant(resultHolder));
+
+      assertTrue(reader.next());
+      variantReader = reader.reader();
+      variantReader.read(resultHolder);
+      assertEquals(variant2, new Variant(resultHolder));
+    }
+  }
+
+  @Test
+  public void testCopyFromForVariantExtensionType() {
+    try (ListVector inVector = ListVector.empty("input", allocator);
+        ListVector outVector = ListVector.empty("output", allocator)) {
+      Variant variant1 = TestVariant.variantString("hello");
+      Variant variant2 = TestVariant.variantString("bye");
+
+      UnionListWriter writer = inVector.getWriter();
+      writer.allocate();
+
+      writer.setPosition(0);
+      writer.startList();
+      ExtensionWriter extensionWriter = writer.extension(VariantType.INSTANCE);
+      extensionWriter.writeExtension(variant1);
+      writer.endList();
+
+      writer.setPosition(1);
+      writer.startList();
+      extensionWriter.writeExtension(variant2);
+      extensionWriter.writeExtension(variant2);
+      writer.endList();
+
+      inVector.setValueCount(2);
+
+      outVector.allocateNew();
+      outVector.copyFrom(0, 0, inVector);
+      outVector.copyFrom(1, 1, inVector);
+      outVector.setValueCount(2);
+
+      ArrayList<Variant> resultSet0 = (ArrayList<Variant>) 
outVector.getObject(0);
+      assertEquals(1, resultSet0.size());
+      assertEquals(variant1, resultSet0.get(0));
+
+      ArrayList<Variant> resultSet1 = (ArrayList<Variant>) 
outVector.getObject(1);
+      assertEquals(2, resultSet1.size());
+      assertEquals(variant2, resultSet1.get(0));
+      assertEquals(variant2, resultSet1.get(1));
+    }
+  }
+
+  @Test
+  public void testCopyValueSafeForVariantExtensionType() {
+    try (ListVector inVector = ListVector.empty("input", allocator)) {
+      Variant variant1 = TestVariant.variantString("hello");
+      Variant variant2 = TestVariant.variantString("bye");
+
+      UnionListWriter writer = inVector.getWriter();
+      writer.allocate();
+
+      writer.setPosition(0);
+      writer.startList();
+      ExtensionWriter extensionWriter = writer.extension(VariantType.INSTANCE);
+      extensionWriter.writeExtension(variant1);
+      writer.endList();
+
+      writer.setPosition(1);
+      writer.startList();
+      extensionWriter.writeExtension(variant2);
+      extensionWriter.writeExtension(variant2);
+      writer.endList();
+
+      inVector.setValueCount(2);
+
+      try (ListVector outVector = (ListVector) 
inVector.getTransferPair(allocator).getTo()) {
+        TransferPair tp = inVector.makeTransferPair(outVector);
+        tp.copyValueSafe(0, 0);
+        tp.copyValueSafe(1, 1);
+        outVector.setValueCount(2);
+
+        ArrayList<Variant> resultSet0 = (ArrayList<Variant>) 
outVector.getObject(0);
+        assertEquals(1, resultSet0.size());
+        assertEquals(variant1, resultSet0.get(0));
+
+        ArrayList<Variant> resultSet1 = (ArrayList<Variant>) 
outVector.getObject(1);
+        assertEquals(2, resultSet1.size());
+        assertEquals(variant2, resultSet1.get(0));
+        assertEquals(variant2, resultSet1.get(1));
+      }
+    }
+  }
+}
diff --git 
a/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantInMapVector.java
 
b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantInMapVector.java
new file mode 100644
index 000000000..dd925810d
--- /dev/null
+++ 
b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantInMapVector.java
@@ -0,0 +1,125 @@
+/*
+ * 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.variant.extension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.variant.TestVariant;
+import org.apache.arrow.variant.Variant;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.vector.complex.MapVector;
+import org.apache.arrow.vector.complex.impl.UnionMapReader;
+import org.apache.arrow.vector.complex.impl.UnionMapWriter;
+import org.apache.arrow.vector.complex.reader.FieldReader;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class TestVariantInMapVector {
+
+  private BufferAllocator allocator;
+
+  @BeforeEach
+  public void init() {
+    allocator = new RootAllocator(Long.MAX_VALUE);
+  }
+
+  @AfterEach
+  public void terminate() {
+    allocator.close();
+  }
+
+  @Test
+  public void testMapVectorWithVariantExtensionType() {
+    Variant variant1 = TestVariant.variantString("hello");
+    Variant variant2 = TestVariant.variantString("world");
+    try (final MapVector inVector = MapVector.empty("map", allocator, false)) {
+      inVector.allocateNew();
+      UnionMapWriter writer = inVector.getWriter();
+      writer.setPosition(0);
+
+      writer.startMap();
+      writer.startEntry();
+      writer.key().bigInt().writeBigInt(0);
+      writer.value().extension(VariantType.INSTANCE).writeExtension(variant1, 
VariantType.INSTANCE);
+      writer.endEntry();
+      writer.startEntry();
+      writer.key().bigInt().writeBigInt(1);
+      writer.value().extension(VariantType.INSTANCE).writeExtension(variant2, 
VariantType.INSTANCE);
+      writer.endEntry();
+      writer.endMap();
+
+      writer.setValueCount(1);
+
+      UnionMapReader mapReader = inVector.getReader();
+      mapReader.setPosition(0);
+      mapReader.next();
+      FieldReader variantReader = mapReader.value();
+      NullableVariantHolder holder = new NullableVariantHolder();
+      variantReader.read(holder);
+      assertEquals(variant1, new Variant(holder));
+
+      mapReader.next();
+      variantReader = mapReader.value();
+      variantReader.read(holder);
+      assertEquals(variant2, new Variant(holder));
+    }
+  }
+
+  @Test
+  public void testCopyFromForVariantExtensionType() {
+    Variant variant1 = TestVariant.variantString("hello");
+    Variant variant2 = TestVariant.variantString("world");
+    try (final MapVector inVector = MapVector.empty("in", allocator, false);
+        final MapVector outVector = MapVector.empty("out", allocator, false)) {
+      inVector.allocateNew();
+      UnionMapWriter writer = inVector.getWriter();
+      writer.setPosition(0);
+
+      writer.startMap();
+      writer.startEntry();
+      writer.key().bigInt().writeBigInt(0);
+      writer.value().extension(VariantType.INSTANCE).writeExtension(variant1, 
VariantType.INSTANCE);
+      writer.endEntry();
+      writer.startEntry();
+      writer.key().bigInt().writeBigInt(1);
+      writer.value().extension(VariantType.INSTANCE).writeExtension(variant2, 
VariantType.INSTANCE);
+      writer.endEntry();
+      writer.endMap();
+
+      writer.setValueCount(1);
+      outVector.allocateNew();
+      outVector.copyFrom(0, 0, inVector);
+      outVector.setValueCount(1);
+
+      UnionMapReader mapReader = outVector.getReader();
+      mapReader.setPosition(0);
+      mapReader.next();
+      FieldReader variantReader = mapReader.value();
+      NullableVariantHolder holder = new NullableVariantHolder();
+      variantReader.read(holder);
+      assertEquals(variant1, new Variant(holder));
+
+      mapReader.next();
+      variantReader = mapReader.value();
+      variantReader.read(holder);
+      assertEquals(variant2, new Variant(holder));
+    }
+  }
+}
diff --git 
a/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantType.java
 
b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantType.java
new file mode 100644
index 000000000..017e71224
--- /dev/null
+++ 
b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantType.java
@@ -0,0 +1,308 @@
+/*
+ * 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.variant.extension;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+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.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+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.nio.ByteBuffer;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.dictionary.DictionaryProvider;
+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.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TestVariantType {
+  BufferAllocator allocator;
+
+  @BeforeEach
+  void beforeEach() {
+    allocator = new RootAllocator();
+  }
+
+  @AfterEach
+  void afterEach() {
+    allocator.close();
+  }
+
+  @Test
+  void testConstants() {
+    assertNotNull(VariantType.INSTANCE);
+  }
+
+  @Test
+  void testStorageType() {
+    VariantType type = VariantType.INSTANCE;
+    assertEquals(ArrowType.Struct.INSTANCE, type.storageType());
+    assertInstanceOf(ArrowType.Struct.class, type.storageType());
+  }
+
+  @Test
+  void testExtensionName() {
+    VariantType type = VariantType.INSTANCE;
+    assertEquals("parquet.variant", type.extensionName());
+  }
+
+  @Test
+  void testExtensionEquals() {
+    VariantType type1 = VariantType.INSTANCE;
+    VariantType type2 = VariantType.INSTANCE;
+
+    assertTrue(type1.extensionEquals(type2));
+  }
+
+  @Test
+  void testIsComplex() {
+    VariantType type = VariantType.INSTANCE;
+    assertFalse(type.isComplex());
+  }
+
+  @Test
+  void testSerialize() {
+    VariantType type = VariantType.INSTANCE;
+    String serialized = type.serialize();
+    assertEquals("", serialized);
+  }
+
+  @Test
+  void testDeserializeValid() {
+    VariantType type = VariantType.INSTANCE;
+    ArrowType storageType = ArrowType.Struct.INSTANCE;
+
+    ArrowType deserialized = assertDoesNotThrow(() -> 
type.deserialize(storageType, ""));
+    assertInstanceOf(VariantType.class, deserialized);
+    assertEquals(VariantType.INSTANCE, deserialized);
+  }
+
+  @Test
+  void testDeserializeInvalidStorageType() {
+    VariantType type = VariantType.INSTANCE;
+    ArrowType wrongStorageType = ArrowType.Utf8.INSTANCE;
+
+    assertThrows(UnsupportedOperationException.class, () -> 
type.deserialize(wrongStorageType, ""));
+  }
+
+  @Test
+  void testGetNewVector() {
+    VariantType type = VariantType.INSTANCE;
+    try (FieldVector vector =
+        type.getNewVector("variant_field", FieldType.nullable(type), 
allocator)) {
+      assertInstanceOf(VariantVector.class, vector);
+      assertEquals("variant_field", vector.getField().getName());
+      assertEquals(type, vector.getField().getType());
+    }
+  }
+
+  @Test
+  void testGetNewVectorWithNullableFieldType() {
+    VariantType type = VariantType.INSTANCE;
+    FieldType nullableFieldType = FieldType.nullable(type);
+
+    try (FieldVector vector = type.getNewVector("nullable_variant", 
nullableFieldType, allocator)) {
+      assertInstanceOf(VariantVector.class, vector);
+      assertEquals("nullable_variant", vector.getField().getName());
+      assertTrue(vector.getField().isNullable());
+    }
+  }
+
+  @Test
+  void testGetNewVectorWithNonNullableFieldType() {
+    VariantType type = VariantType.INSTANCE;
+    FieldType nonNullableFieldType = FieldType.notNullable(type);
+
+    try (FieldVector vector =
+        type.getNewVector("non_nullable_variant", nonNullableFieldType, 
allocator)) {
+      assertInstanceOf(VariantVector.class, vector);
+      assertEquals("non_nullable_variant", vector.getField().getName());
+    }
+  }
+
+  @Test
+  void testIpcRoundTrip() {
+    VariantType type = VariantType.INSTANCE;
+
+    Schema schema = new 
Schema(Collections.singletonList(Field.nullable("variant", type)));
+    byte[] serialized = schema.serializeAsMessage();
+    Schema deserialized = 
Schema.deserializeMessage(ByteBuffer.wrap(serialized));
+    assertEquals(schema, deserialized);
+  }
+
+  @Test
+  void testVectorIpcRoundTrip() throws IOException {
+    VariantType type = VariantType.INSTANCE;
+
+    try (FieldVector vector = type.getNewVector("field", 
FieldType.nullable(type), allocator);
+        ArrowBuf metadataBuf1 = allocator.buffer(10);
+        ArrowBuf valueBuf1 = allocator.buffer(10);
+        ArrowBuf metadataBuf2 = allocator.buffer(10);
+        ArrowBuf valueBuf2 = allocator.buffer(10)) {
+      VariantVector variantVector = (VariantVector) vector;
+
+      byte[] metadata1 = new byte[] {1, 2, 3};
+      byte[] value1 = new byte[] {4, 5, 6, 7};
+      metadataBuf1.setBytes(0, metadata1);
+      valueBuf1.setBytes(0, value1);
+
+      byte[] metadata2 = new byte[] {8, 9};
+      byte[] value2 = new byte[] {10, 11, 12};
+      metadataBuf2.setBytes(0, metadata2);
+      valueBuf2.setBytes(0, value2);
+
+      NullableVariantHolder holder1 = new NullableVariantHolder();
+      holder1.isSet = 1;
+      holder1.metadataStart = 0;
+      holder1.metadataEnd = metadata1.length;
+      holder1.metadataBuffer = metadataBuf1;
+      holder1.valueStart = 0;
+      holder1.valueEnd = value1.length;
+      holder1.valueBuffer = valueBuf1;
+
+      NullableVariantHolder holder2 = new NullableVariantHolder();
+      holder2.isSet = 1;
+      holder2.metadataStart = 0;
+      holder2.metadataEnd = metadata2.length;
+      holder2.metadataBuffer = metadataBuf2;
+      holder2.valueStart = 0;
+      holder2.valueEnd = value2.length;
+      holder2.valueBuffer = valueBuf2;
+
+      variantVector.setSafe(0, holder1);
+      variantVector.setNull(1);
+      variantVector.setSafe(2, holder2);
+      variantVector.setValueCount(3);
+
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      try (VectorSchemaRoot root = new 
VectorSchemaRoot(Collections.singletonList(variantVector));
+          ArrowStreamWriter writer =
+              new ArrowStreamWriter(root, new 
DictionaryProvider.MapDictionaryProvider(), baos)) {
+        writer.start();
+        writer.writeBatch();
+      }
+
+      try (ArrowStreamReader reader =
+          new ArrowStreamReader(new ByteArrayInputStream(baos.toByteArray()), 
allocator)) {
+        assertTrue(reader.loadNextBatch());
+        VectorSchemaRoot root = reader.getVectorSchemaRoot();
+        assertEquals(3, root.getRowCount());
+        assertEquals(
+            new Schema(Collections.singletonList(variantVector.getField())), 
root.getSchema());
+
+        VariantVector actual = assertInstanceOf(VariantVector.class, 
root.getVector("field"));
+        assertFalse(actual.isNull(0));
+        assertTrue(actual.isNull(1));
+        assertFalse(actual.isNull(2));
+
+        NullableVariantHolder result1 = new NullableVariantHolder();
+        actual.get(0, result1);
+        assertEquals(1, result1.isSet);
+        assertEquals(metadata1.length, result1.metadataEnd - 
result1.metadataStart);
+        assertEquals(value1.length, result1.valueEnd - result1.valueStart);
+
+        assertNull(actual.getObject(1));
+
+        NullableVariantHolder result2 = new NullableVariantHolder();
+        actual.get(2, result2);
+        assertEquals(1, result2.isSet);
+        assertEquals(metadata2.length, result2.metadataEnd - 
result2.metadataStart);
+        assertEquals(value2.length, result2.valueEnd - result2.valueStart);
+      }
+    }
+  }
+
+  @Test
+  void testSingleton() {
+    VariantType type1 = VariantType.INSTANCE;
+    VariantType type2 = VariantType.INSTANCE;
+
+    // Same instance
+    assertSame(type1, type2);
+    assertTrue(type1.extensionEquals(type2));
+  }
+
+  @Test
+  void testExtensionTypeRegistry() {
+    // VariantType should be automatically registered via static initializer
+    ArrowType.ExtensionType registeredType =
+        ExtensionTypeRegistry.lookup(VariantType.EXTENSION_NAME);
+    assertNotNull(registeredType);
+    assertInstanceOf(VariantType.class, registeredType);
+    assertEquals(VariantType.INSTANCE, registeredType);
+  }
+
+  @Test
+  void testFieldMetadata() {
+    Map<String, String> metadata = new HashMap<>();
+    metadata.put("key1", "value1");
+    metadata.put("key2", "value2");
+
+    FieldType fieldType = new FieldType(true, VariantType.INSTANCE, null, 
metadata);
+    try (VariantVector vector = new VariantVector("test", allocator)) {
+      Field field = new Field("test", fieldType, 
VariantVector.createVariantChildFields());
+
+      // Field metadata includes both custom metadata and extension type 
metadata
+      Map<String, String> fieldMetadata = field.getMetadata();
+      assertEquals("value1", fieldMetadata.get("key1"));
+      assertEquals("value2", fieldMetadata.get("key2"));
+      // Extension type metadata is also present
+      assertTrue(fieldMetadata.containsKey("ARROW:extension:name"));
+      assertTrue(fieldMetadata.containsKey("ARROW:extension:metadata"));
+    }
+  }
+
+  @Test
+  void testFieldChildren() {
+    try (VariantVector vector = new VariantVector("test", allocator)) {
+      Field field = vector.getField();
+
+      assertNotNull(field.getChildren());
+      assertEquals(2, field.getChildren().size());
+
+      Field metadataField = field.getChildren().get(0);
+      assertEquals(VariantVector.METADATA_VECTOR_NAME, 
metadataField.getName());
+      assertEquals(ArrowType.Binary.INSTANCE, metadataField.getType());
+
+      Field valueField = field.getChildren().get(1);
+      assertEquals(VariantVector.VALUE_VECTOR_NAME, valueField.getName());
+      assertEquals(ArrowType.Binary.INSTANCE, valueField.getType());
+    }
+  }
+}
diff --git 
a/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantVector.java
 
b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantVector.java
new file mode 100644
index 000000000..1c172e304
--- /dev/null
+++ 
b/arrow-variant/src/test/java/org/apache/arrow/variant/extension/TestVariantVector.java
@@ -0,0 +1,844 @@
+/*
+ * 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.variant.extension;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.arrow.variant.Variant;
+import org.apache.arrow.variant.holders.NullableVariantHolder;
+import org.apache.arrow.variant.holders.VariantHolder;
+import org.apache.arrow.variant.impl.VariantReaderImpl;
+import org.apache.arrow.variant.impl.VariantWriterImpl;
+import org.apache.arrow.vector.holders.ExtensionHolder;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Tests for VariantVector, VariantWriterImpl, and VariantReaderImpl. */
+class TestVariantVector {
+
+  private BufferAllocator allocator;
+
+  @BeforeEach
+  void beforeEach() {
+    allocator = new RootAllocator();
+  }
+
+  @AfterEach
+  void afterEach() {
+    allocator.close();
+  }
+
+  private VariantHolder createHolder(
+      ArrowBuf metadataBuf, byte[] metadata, ArrowBuf valueBuf, byte[] value) {
+    VariantHolder holder = new VariantHolder();
+    holder.metadataStart = 0;
+    holder.metadataEnd = metadata.length;
+    holder.metadataBuffer = metadataBuf;
+    holder.valueStart = 0;
+    holder.valueEnd = value.length;
+    holder.valueBuffer = valueBuf;
+    return holder;
+  }
+
+  private NullableVariantHolder createNullableHolder(
+      ArrowBuf metadataBuf, byte[] metadata, ArrowBuf valueBuf, byte[] value) {
+    NullableVariantHolder holder = new NullableVariantHolder();
+    holder.isSet = 1;
+    holder.metadataStart = 0;
+    holder.metadataEnd = metadata.length;
+    holder.metadataBuffer = metadataBuf;
+    holder.valueStart = 0;
+    holder.valueEnd = value.length;
+    holder.valueBuffer = valueBuf;
+    return holder;
+  }
+
+  private NullableVariantHolder createNullHolder() {
+    NullableVariantHolder holder = new NullableVariantHolder();
+    holder.isSet = 0;
+    return holder;
+  }
+
+  // ========== Basic Vector Tests ==========
+
+  @Test
+  void testVectorCreation() {
+    try (VariantVector vector = new VariantVector("test", allocator)) {
+      assertNotNull(vector);
+      assertEquals("test", vector.getField().getName());
+      assertNotNull(vector.getMetadataVector());
+      assertNotNull(vector.getValueVector());
+    }
+  }
+
+  @Test
+  void testSetAndGet() {
+    try (VariantVector vector = new VariantVector("test", allocator);
+        ArrowBuf metadataBuf = allocator.buffer(10);
+        ArrowBuf valueBuf = allocator.buffer(10)) {
+
+      byte[] metadata = new byte[] {1, 2, 3};
+      byte[] value = new byte[] {4, 5, 6, 7};
+      metadataBuf.setBytes(0, metadata);
+      valueBuf.setBytes(0, value);
+
+      NullableVariantHolder holder = createNullableHolder(metadataBuf, 
metadata, valueBuf, value);
+
+      vector.setSafe(0, holder);
+      vector.setValueCount(1);
+
+      // Retrieve and verify
+      NullableVariantHolder result = new NullableVariantHolder();
+      vector.get(0, result);
+
+      assertEquals(1, result.isSet);
+      assertEquals(metadata.length, result.metadataEnd - result.metadataStart);
+      assertEquals(value.length, result.valueEnd - result.valueStart);
+
+      byte[] actualMetadata = new byte[metadata.length];
+      byte[] actualValue = new byte[value.length];
+      result.metadataBuffer.getBytes(result.metadataStart, actualMetadata);
+      result.valueBuffer.getBytes(result.valueStart, actualValue);
+
+      assertArrayEquals(metadata, actualMetadata);
+      assertArrayEquals(value, actualValue);
+    }
+  }
+
+  @Test
+  void testSetNull() {
+    try (VariantVector vector = new VariantVector("test", allocator)) {
+      NullableVariantHolder holder = createNullHolder();
+
+      vector.setSafe(0, holder);
+      vector.setValueCount(1);
+
+      assertTrue(vector.isNull(0));
+
+      NullableVariantHolder result = new NullableVariantHolder();
+      vector.get(0, result);
+      assertEquals(0, result.isSet);
+    }
+  }
+
+  @Test
+  void testMultipleValues() {
+    try (VariantVector vector = new VariantVector("test", allocator);
+        ArrowBuf metadataBuf1 = allocator.buffer(10);
+        ArrowBuf valueBuf1 = allocator.buffer(10);
+        ArrowBuf metadataBuf2 = allocator.buffer(10);
+        ArrowBuf valueBuf2 = allocator.buffer(10)) {
+
+      byte[] metadata1 = new byte[] {1, 2};
+      byte[] value1 = new byte[] {3, 4, 5};
+      metadataBuf1.setBytes(0, metadata1);
+      valueBuf1.setBytes(0, value1);
+
+      NullableVariantHolder holder1 =
+          createNullableHolder(metadataBuf1, metadata1, valueBuf1, value1);
+
+      byte[] metadata2 = new byte[] {6, 7, 8};
+      byte[] value2 = new byte[] {9, 10};
+      metadataBuf2.setBytes(0, metadata2);
+      valueBuf2.setBytes(0, value2);
+
+      NullableVariantHolder holder2 =
+          createNullableHolder(metadataBuf2, metadata2, valueBuf2, value2);
+
+      vector.setSafe(0, holder1);
+      vector.setSafe(1, holder2);
+      vector.setValueCount(2);
+
+      // Verify first value
+      NullableVariantHolder result1 = new NullableVariantHolder();
+      vector.get(0, result1);
+      assertEquals(1, result1.isSet);
+
+      byte[] actualMetadata1 = new byte[metadata1.length];
+      byte[] actualValue1 = new byte[value1.length];
+      result1.metadataBuffer.getBytes(result1.metadataStart, actualMetadata1);
+      result1.valueBuffer.getBytes(result1.valueStart, actualValue1);
+      assertArrayEquals(metadata1, actualMetadata1);
+      assertArrayEquals(value1, actualValue1);
+
+      // Verify second value
+      NullableVariantHolder result2 = new NullableVariantHolder();
+      vector.get(1, result2);
+      assertEquals(1, result2.isSet);
+
+      byte[] actualMetadata2 = new byte[metadata2.length];
+      byte[] actualValue2 = new byte[value2.length];
+      result2.metadataBuffer.getBytes(result2.metadataStart, actualMetadata2);
+      result2.valueBuffer.getBytes(result2.valueStart, actualValue2);
+      assertArrayEquals(metadata2, actualMetadata2);
+      assertArrayEquals(value2, actualValue2);
+    }
+  }
+
+  @Test
+  void testNonNullableHolder() {
+    try (VariantVector vector = new VariantVector("test", allocator);
+        ArrowBuf metadataBuf = allocator.buffer(10);
+        ArrowBuf valueBuf = allocator.buffer(10)) {
+
+      byte[] metadata = new byte[] {1, 2, 3};
+      byte[] value = new byte[] {4, 5, 6};
+      metadataBuf.setBytes(0, metadata);
+      valueBuf.setBytes(0, value);
+
+      VariantHolder holder = createHolder(metadataBuf, metadata, valueBuf, 
value);
+
+      vector.setSafe(0, holder);
+      vector.setValueCount(1);
+
+      assertFalse(vector.isNull(0));
+
+      NullableVariantHolder result = new NullableVariantHolder();
+      vector.get(0, result);
+      assertEquals(1, result.isSet);
+    }
+  }
+
+  // ========== Writer Tests ==========
+
+  @Test
+  void testWriteWithVariantHolder() {
+    try (VariantVector vector = new VariantVector("test", allocator);
+        VariantWriterImpl writer = new VariantWriterImpl(vector);
+        ArrowBuf metadataBuf = allocator.buffer(10);
+        ArrowBuf valueBuf = allocator.buffer(10)) {
+
+      byte[] metadata = new byte[] {1, 2};
+      byte[] value = new byte[] {3, 4, 5};
+      metadataBuf.setBytes(0, metadata);
+      valueBuf.setBytes(0, value);
+
+      VariantHolder holder = createHolder(metadataBuf, metadata, valueBuf, 
value);
+
+      writer.setPosition(0);
+      writer.write(holder);
+
+      assertEquals(1, vector.getValueCount());
+      assertFalse(vector.isNull(0));
+    }
+  }
+
+  @Test
+  void testWriteWithNullableVariantHolder() {
+    try (VariantVector vector = new VariantVector("test", allocator);
+        VariantWriterImpl writer = new VariantWriterImpl(vector);
+        ArrowBuf metadataBuf = allocator.buffer(10);
+        ArrowBuf valueBuf = allocator.buffer(10)) {
+
+      byte[] metadata = new byte[] {1, 2};
+      byte[] value = new byte[] {3, 4, 5};
+      metadataBuf.setBytes(0, metadata);
+      valueBuf.setBytes(0, value);
+
+      NullableVariantHolder holder = createNullableHolder(metadataBuf, 
metadata, valueBuf, value);
+
+      writer.setPosition(0);
+      writer.write(holder);
+
+      assertEquals(1, vector.getValueCount());
+      assertFalse(vector.isNull(0));
+    }
+  }
+
+  @Test
+  void testWriteWithNullableVariantHolderNull() {
+    try (VariantVector vector = new VariantVector("test", allocator);
+        VariantWriterImpl writer = new VariantWriterImpl(vector)) {
+
+      NullableVariantHolder holder = createNullHolder();
+
+      writer.setPosition(0);
+      writer.write(holder);
+
+      assertEquals(1, vector.getValueCount());
+      assertTrue(vector.isNull(0));
+    }
+  }
+
+  @Test
+  void testWriteExtensionWithUnsupportedType() {
+    try (VariantVector vector = new VariantVector("test", allocator);
+        VariantWriterImpl writer = new VariantWriterImpl(vector)) {
+
+      writer.setPosition(0);
+
+      IllegalArgumentException exception =
+          assertThrows(IllegalArgumentException.class, () -> 
writer.writeExtension("invalid-type"));
+
+      assertTrue(exception.getMessage().contains("Unsupported type for 
Variant"));
+    }
+  }
+
+  @Test
+  void testWriteWithUnsupportedHolder() {
+    try (VariantVector vector = new VariantVector("test", allocator);
+        VariantWriterImpl writer = new VariantWriterImpl(vector)) {
+
+      ExtensionHolder unsupportedHolder =
+          new ExtensionHolder() {
+            @Override
+            public ArrowType type() {
+              return VariantType.INSTANCE;
+            }
+          };
+
+      writer.setPosition(0);
+
+      IllegalArgumentException exception =
+          assertThrows(IllegalArgumentException.class, () -> 
writer.write(unsupportedHolder));
+
+      assertTrue(exception.getMessage().contains("Unsupported type for 
Variant"));
+    }
+  }
+
+  // ========== Reader Tests ==========
+
+  @Test
+  void testReaderReadWithNullableVariantHolder() {
+    try (VariantVector vector = new VariantVector("test", allocator);
+        ArrowBuf metadataBuf = allocator.buffer(10);
+        ArrowBuf valueBuf = allocator.buffer(10)) {
+
+      byte[] metadata = new byte[] {1, 2, 3};
+      byte[] value = new byte[] {4, 5, 6};
+      metadataBuf.setBytes(0, metadata);
+      valueBuf.setBytes(0, value);
+
+      NullableVariantHolder holder = createNullableHolder(metadataBuf, 
metadata, valueBuf, value);
+
+      vector.setSafe(0, holder);
+      vector.setValueCount(1);
+
+      VariantReaderImpl reader = (VariantReaderImpl) vector.getReader();
+      reader.setPosition(0);
+
+      NullableVariantHolder result = new NullableVariantHolder();
+      reader.read(result);
+
+      assertEquals(1, result.isSet);
+      assertEquals(metadata.length, result.metadataEnd - result.metadataStart);
+      assertEquals(value.length, result.valueEnd - result.valueStart);
+    }
+  }
+
+  @Test
+  void testReaderReadWithNullableVariantHolderNull() {
+    try (VariantVector vector = new VariantVector("test", allocator)) {
+      vector.setNull(0);
+      vector.setValueCount(1);
+
+      VariantReaderImpl reader = (VariantReaderImpl) vector.getReader();
+      reader.setPosition(0);
+
+      NullableVariantHolder holder = new NullableVariantHolder();
+      reader.read(holder);
+
+      assertEquals(0, holder.isSet);
+    }
+  }
+
+  @Test
+  void testReaderIsSet() {
+    try (VariantVector vector = new VariantVector("test", allocator);
+        ArrowBuf metadataBuf = allocator.buffer(10);
+        ArrowBuf valueBuf = allocator.buffer(10)) {
+
+      byte[] metadata = new byte[] {1};
+      byte[] value = new byte[] {2};
+      metadataBuf.setBytes(0, metadata);
+      valueBuf.setBytes(0, value);
+
+      NullableVariantHolder holder = createNullableHolder(metadataBuf, 
metadata, valueBuf, value);
+
+      vector.setSafe(0, holder);
+      vector.setNull(1);
+      vector.setValueCount(2);
+
+      VariantReaderImpl reader = (VariantReaderImpl) vector.getReader();
+
+      reader.setPosition(0);
+      assertTrue(reader.isSet());
+
+      reader.setPosition(1);
+      assertFalse(reader.isSet());
+    }
+  }
+
+  @Test
+  void testReaderGetMinorType() {
+    try (VariantVector vector = new VariantVector("test", allocator)) {
+      VariantReaderImpl reader = (VariantReaderImpl) vector.getReader();
+      assertEquals(vector.getMinorType(), reader.getMinorType());
+    }
+  }
+
+  @Test
+  void testReaderGetField() {
+    try (VariantVector vector = new VariantVector("test", allocator)) {
+      VariantReaderImpl reader = (VariantReaderImpl) vector.getReader();
+      assertEquals(vector.getField(), reader.getField());
+      assertEquals("test", reader.getField().getName());
+    }
+  }
+
+  @Test
+  void testReaderReadWithNonNullableVariantHolder() {
+    try (VariantVector vector = new VariantVector("test", allocator);
+        ArrowBuf metadataBuf = allocator.buffer(10);
+        ArrowBuf valueBuf = allocator.buffer(10)) {
+
+      byte[] metadata = new byte[] {1, 2, 3};
+      byte[] value = new byte[] {4, 5, 6};
+      metadataBuf.setBytes(0, metadata);
+      valueBuf.setBytes(0, value);
+
+      NullableVariantHolder holder = createNullableHolder(metadataBuf, 
metadata, valueBuf, value);
+
+      vector.setSafe(0, holder);
+      vector.setValueCount(1);
+
+      VariantReaderImpl reader = (VariantReaderImpl) vector.getReader();
+      reader.setPosition(0);
+
+      VariantHolder result = new VariantHolder();
+      reader.read(result);
+
+      // Verify the data was read correctly
+      byte[] actualMetadata = new byte[metadata.length];
+      byte[] actualValue = new byte[value.length];
+      result.metadataBuffer.getBytes(result.metadataStart, actualMetadata);
+      result.valueBuffer.getBytes(result.valueStart, actualValue);
+
+      assertArrayEquals(metadata, actualMetadata);
+      assertArrayEquals(value, actualValue);
+      assertEquals(1, result.isSet);
+    }
+  }
+
+  // ========== Transfer Pair Tests ==========
+
+  @Test
+  void testTransferPair() {
+    try (VariantVector fromVector = new VariantVector("from", allocator);
+        ArrowBuf metadataBuf = allocator.buffer(10);
+        ArrowBuf valueBuf = allocator.buffer(10)) {
+
+      byte[] metadata = new byte[] {1, 2, 3};
+      byte[] value = new byte[] {4, 5, 6, 7};
+      metadataBuf.setBytes(0, metadata);
+      valueBuf.setBytes(0, value);
+
+      NullableVariantHolder holder = createNullableHolder(metadataBuf, 
metadata, valueBuf, value);
+
+      fromVector.setSafe(0, holder);
+      fromVector.setValueCount(1);
+
+      org.apache.arrow.vector.util.TransferPair transferPair =
+          fromVector.getTransferPair(allocator);
+      VariantVector toVector = (VariantVector) transferPair.getTo();
+
+      transferPair.transfer();
+
+      assertEquals(0, fromVector.getValueCount());
+      assertEquals(1, toVector.getValueCount());
+
+      NullableVariantHolder result = new NullableVariantHolder();
+      toVector.get(0, result);
+      assertEquals(1, result.isSet);
+
+      byte[] actualMetadata = new byte[metadata.length];
+      byte[] actualValue = new byte[value.length];
+      result.metadataBuffer.getBytes(result.metadataStart, actualMetadata);
+      result.valueBuffer.getBytes(result.valueStart, actualValue);
+
+      assertArrayEquals(metadata, actualMetadata);
+      assertArrayEquals(value, actualValue);
+
+      toVector.close();
+    }
+  }
+
+  @Test
+  void testSplitAndTransfer() {
+    try (VariantVector fromVector = new VariantVector("from", allocator);
+        ArrowBuf metadataBuf1 = allocator.buffer(10);
+        ArrowBuf valueBuf1 = allocator.buffer(10);
+        ArrowBuf metadataBuf2 = allocator.buffer(10);
+        ArrowBuf valueBuf2 = allocator.buffer(10);
+        ArrowBuf metadataBuf3 = allocator.buffer(10);
+        ArrowBuf valueBuf3 = allocator.buffer(10)) {
+
+      byte[] metadata1 = new byte[] {1};
+      byte[] value1 = new byte[] {2, 3};
+      metadataBuf1.setBytes(0, metadata1);
+      valueBuf1.setBytes(0, value1);
+
+      byte[] metadata2 = new byte[] {4, 5};
+      byte[] value2 = new byte[] {6};
+      metadataBuf2.setBytes(0, metadata2);
+      valueBuf2.setBytes(0, value2);
+
+      byte[] metadata3 = new byte[] {7, 8, 9};
+      byte[] value3 = new byte[] {10, 11, 12};
+      metadataBuf3.setBytes(0, metadata3);
+      valueBuf3.setBytes(0, value3);
+
+      NullableVariantHolder holder1 =
+          createNullableHolder(metadataBuf1, metadata1, valueBuf1, value1);
+      NullableVariantHolder holder2 =
+          createNullableHolder(metadataBuf2, metadata2, valueBuf2, value2);
+      NullableVariantHolder holder3 =
+          createNullableHolder(metadataBuf3, metadata3, valueBuf3, value3);
+
+      fromVector.setSafe(0, holder1);
+      fromVector.setSafe(1, holder2);
+      fromVector.setSafe(2, holder3);
+      fromVector.setValueCount(3);
+
+      org.apache.arrow.vector.util.TransferPair transferPair =
+          fromVector.getTransferPair(allocator);
+      VariantVector toVector = (VariantVector) transferPair.getTo();
+
+      // Split and transfer indices 1-2 (middle and last)
+      transferPair.splitAndTransfer(1, 2);
+
+      assertEquals(2, toVector.getValueCount());
+
+      // Verify transferred values
+      NullableVariantHolder result1 = new NullableVariantHolder();
+      toVector.get(0, result1);
+      assertEquals(1, result1.isSet);
+
+      byte[] actualMetadata1 = new byte[metadata2.length];
+      byte[] actualValue1 = new byte[value2.length];
+      result1.metadataBuffer.getBytes(result1.metadataStart, actualMetadata1);
+      result1.valueBuffer.getBytes(result1.valueStart, actualValue1);
+      assertArrayEquals(metadata2, actualMetadata1);
+      assertArrayEquals(value2, actualValue1);
+
+      NullableVariantHolder result2 = new NullableVariantHolder();
+      toVector.get(1, result2);
+      assertEquals(1, result2.isSet);
+
+      byte[] actualMetadata2 = new byte[metadata3.length];
+      byte[] actualValue2 = new byte[value3.length];
+      result2.metadataBuffer.getBytes(result2.metadataStart, actualMetadata2);
+      result2.valueBuffer.getBytes(result2.valueStart, actualValue2);
+      assertArrayEquals(metadata3, actualMetadata2);
+      assertArrayEquals(value3, actualValue2);
+
+      toVector.close();
+    }
+  }
+
+  @Test
+  void testCopyValueSafe() {
+    try (VariantVector fromVector = new VariantVector("from", allocator);
+        VariantVector toVector = new VariantVector("to", allocator);
+        ArrowBuf metadataBuf = allocator.buffer(10);
+        ArrowBuf valueBuf = allocator.buffer(10)) {
+
+      byte[] metadata = new byte[] {1, 2};
+      byte[] value = new byte[] {3, 4, 5};
+      metadataBuf.setBytes(0, metadata);
+      valueBuf.setBytes(0, value);
+
+      NullableVariantHolder holder = createNullableHolder(metadataBuf, 
metadata, valueBuf, value);
+
+      fromVector.setSafe(0, holder);
+      fromVector.setValueCount(1);
+
+      org.apache.arrow.vector.util.TransferPair transferPair =
+          fromVector.makeTransferPair(toVector);
+
+      transferPair.copyValueSafe(0, 0);
+      toVector.setValueCount(1);
+
+      // Verify the value was copied
+      NullableVariantHolder result = new NullableVariantHolder();
+      toVector.get(0, result);
+      assertEquals(1, result.isSet);
+
+      byte[] actualMetadata = new byte[metadata.length];
+      byte[] actualValue = new byte[value.length];
+      result.metadataBuffer.getBytes(result.metadataStart, actualMetadata);
+      result.valueBuffer.getBytes(result.valueStart, actualValue);
+
+      assertArrayEquals(metadata, actualMetadata);
+      assertArrayEquals(value, actualValue);
+
+      // Original vector should still have the value
+      NullableVariantHolder originalResult = new NullableVariantHolder();
+      fromVector.get(0, originalResult);
+      assertEquals(1, originalResult.isSet);
+    }
+  }
+
+  @Test
+  void testGetTransferPairWithField() {
+    try (VariantVector fromVector = new VariantVector("from", allocator);
+        ArrowBuf metadataBuf = allocator.buffer(10);
+        ArrowBuf valueBuf = allocator.buffer(10)) {
+
+      byte[] metadata = new byte[] {1};
+      byte[] value = new byte[] {2};
+      metadataBuf.setBytes(0, metadata);
+      valueBuf.setBytes(0, value);
+
+      NullableVariantHolder holder = createNullableHolder(metadataBuf, 
metadata, valueBuf, value);
+
+      fromVector.setSafe(0, holder);
+      fromVector.setValueCount(1);
+
+      org.apache.arrow.vector.util.TransferPair transferPair =
+          fromVector.getTransferPair(fromVector.getField(), allocator);
+      VariantVector toVector = (VariantVector) transferPair.getTo();
+
+      transferPair.transfer();
+
+      assertEquals(1, toVector.getValueCount());
+      assertEquals(fromVector.getField().getName(), 
toVector.getField().getName());
+
+      toVector.close();
+    }
+  }
+
+  // ========== Copy Operations Tests ==========
+
+  @Test
+  void testCopyFrom() {
+    try (VariantVector fromVector = new VariantVector("from", allocator);
+        VariantVector toVector = new VariantVector("to", allocator);
+        ArrowBuf metadataBuf = allocator.buffer(10);
+        ArrowBuf valueBuf = allocator.buffer(10)) {
+
+      byte[] metadata = new byte[] {1, 2, 3};
+      byte[] value = new byte[] {4, 5};
+      metadataBuf.setBytes(0, metadata);
+      valueBuf.setBytes(0, value);
+
+      NullableVariantHolder holder = createNullableHolder(metadataBuf, 
metadata, valueBuf, value);
+
+      fromVector.setSafe(0, holder);
+      fromVector.setValueCount(1);
+
+      toVector.allocateNew();
+      toVector.copyFrom(0, 0, fromVector);
+      toVector.setValueCount(1);
+
+      NullableVariantHolder result = new NullableVariantHolder();
+      toVector.get(0, result);
+      assertEquals(1, result.isSet);
+
+      byte[] actualMetadata = new byte[metadata.length];
+      byte[] actualValue = new byte[value.length];
+      result.metadataBuffer.getBytes(result.metadataStart, actualMetadata);
+      result.valueBuffer.getBytes(result.valueStart, actualValue);
+
+      assertArrayEquals(metadata, actualMetadata);
+      assertArrayEquals(value, actualValue);
+    }
+  }
+
+  @Test
+  void testCopyFromSafe() {
+    try (VariantVector fromVector = new VariantVector("from", allocator);
+        VariantVector toVector = new VariantVector("to", allocator);
+        ArrowBuf metadataBuf1 = allocator.buffer(10);
+        ArrowBuf valueBuf1 = allocator.buffer(10);
+        ArrowBuf metadataBuf2 = allocator.buffer(10);
+        ArrowBuf valueBuf2 = allocator.buffer(10)) {
+
+      byte[] metadata1 = new byte[] {1};
+      byte[] value1 = new byte[] {2, 3};
+      metadataBuf1.setBytes(0, metadata1);
+      valueBuf1.setBytes(0, value1);
+
+      NullableVariantHolder holder1 =
+          createNullableHolder(metadataBuf1, metadata1, valueBuf1, value1);
+
+      byte[] metadata2 = new byte[] {4, 5};
+      byte[] value2 = new byte[] {6};
+      metadataBuf2.setBytes(0, metadata2);
+      valueBuf2.setBytes(0, value2);
+
+      NullableVariantHolder holder2 =
+          createNullableHolder(metadataBuf2, metadata2, valueBuf2, value2);
+
+      fromVector.setSafe(0, holder1);
+      fromVector.setSafe(1, holder2);
+      fromVector.setValueCount(2);
+
+      // Copy without pre-allocating toVector
+      for (int i = 0; i < 2; i++) {
+        toVector.copyFromSafe(i, i, fromVector);
+      }
+      toVector.setValueCount(2);
+
+      // Verify both values
+      NullableVariantHolder result1 = new NullableVariantHolder();
+      toVector.get(0, result1);
+      assertEquals(1, result1.isSet);
+
+      byte[] actualMetadata1 = new byte[metadata1.length];
+      byte[] actualValue1 = new byte[value1.length];
+      result1.metadataBuffer.getBytes(result1.metadataStart, actualMetadata1);
+      result1.valueBuffer.getBytes(result1.valueStart, actualValue1);
+      assertArrayEquals(metadata1, actualMetadata1);
+      assertArrayEquals(value1, actualValue1);
+
+      NullableVariantHolder result2 = new NullableVariantHolder();
+      toVector.get(1, result2);
+      assertEquals(1, result2.isSet);
+
+      byte[] actualMetadata2 = new byte[metadata2.length];
+      byte[] actualValue2 = new byte[value2.length];
+      result2.metadataBuffer.getBytes(result2.metadataStart, actualMetadata2);
+      result2.valueBuffer.getBytes(result2.valueStart, actualValue2);
+      assertArrayEquals(metadata2, actualMetadata2);
+      assertArrayEquals(value2, actualValue2);
+    }
+  }
+
+  @Test
+  void testCopyFromWithNulls() {
+    try (VariantVector fromVector = new VariantVector("from", allocator);
+        VariantVector toVector = new VariantVector("to", allocator);
+        ArrowBuf metadataBuf = allocator.buffer(10);
+        ArrowBuf valueBuf = allocator.buffer(10)) {
+
+      byte[] metadata = new byte[] {1};
+      byte[] value = new byte[] {2};
+      metadataBuf.setBytes(0, metadata);
+      valueBuf.setBytes(0, value);
+
+      NullableVariantHolder holder = createNullableHolder(metadataBuf, 
metadata, valueBuf, value);
+
+      fromVector.setSafe(0, holder);
+      fromVector.setNull(1);
+      fromVector.setSafe(2, holder);
+      fromVector.setValueCount(3);
+
+      toVector.allocateNew();
+      for (int i = 0; i < 3; i++) {
+        toVector.copyFromSafe(i, i, fromVector);
+      }
+      toVector.setValueCount(3);
+
+      assertFalse(toVector.isNull(0));
+      assertTrue(toVector.isNull(1));
+      assertFalse(toVector.isNull(2));
+    }
+  }
+
+  // ========== GetObject Tests ==========
+
+  @Test
+  void testGetObject() {
+    try (VariantVector vector = new VariantVector("test", allocator);
+        ArrowBuf metadataBuf = allocator.buffer(10);
+        ArrowBuf valueBuf = allocator.buffer(10)) {
+
+      byte[] metadata = new byte[] {1, 2};
+      byte[] value = new byte[] {3, 4, 5};
+      metadataBuf.setBytes(0, metadata);
+      valueBuf.setBytes(0, value);
+
+      NullableVariantHolder holder = createNullableHolder(metadataBuf, 
metadata, valueBuf, value);
+
+      vector.setSafe(0, holder);
+      vector.setValueCount(1);
+
+      Object obj = vector.getObject(0);
+      assertNotNull(obj);
+      assertTrue(obj instanceof Variant);
+      assertEquals(new Variant(metadata, value), obj);
+    }
+  }
+
+  @Test
+  void testGetObjectNull() {
+    try (VariantVector vector = new VariantVector("test", allocator)) {
+      vector.setNull(0);
+      vector.setValueCount(1);
+
+      Object obj = vector.getObject(0);
+      assertNull(obj);
+    }
+  }
+
+  // ========== Allocate and Capacity Tests ==========
+
+  @Test
+  void testAllocateNew() {
+    try (VariantVector vector = new VariantVector("test", allocator)) {
+      vector.allocateNew();
+      assertTrue(vector.getValueCapacity() > 0);
+    }
+  }
+
+  @Test
+  void testSetInitialCapacity() {
+    try (VariantVector vector = new VariantVector("test", allocator)) {
+      vector.setInitialCapacity(100);
+      vector.allocateNew();
+      assertTrue(vector.getValueCapacity() >= 100);
+    }
+  }
+
+  @Test
+  void testClearAndReuse() {
+    try (VariantVector vector = new VariantVector("test", allocator);
+        ArrowBuf metadataBuf = allocator.buffer(10);
+        ArrowBuf valueBuf = allocator.buffer(10)) {
+
+      byte[] metadata = new byte[] {1};
+      byte[] value = new byte[] {2};
+      metadataBuf.setBytes(0, metadata);
+      valueBuf.setBytes(0, value);
+
+      NullableVariantHolder holder = createNullableHolder(metadataBuf, 
metadata, valueBuf, value);
+
+      vector.setSafe(0, holder);
+      vector.setValueCount(1);
+
+      assertFalse(vector.isNull(0));
+
+      vector.clear();
+      vector.allocateNew();
+
+      // After clear, vector should be empty
+      assertEquals(0, vector.getValueCount());
+    }
+  }
+}
diff --git a/bom/pom.xml b/bom/pom.xml
index 9efde5324..b631f9366 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -194,6 +194,11 @@ under the License.
         <artifactId>arrow-tools</artifactId>
         <version>${project.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.apache.arrow</groupId>
+        <artifactId>arrow-variant</artifactId>
+        <version>${project.version}</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
diff --git a/pom.xml b/pom.xml
index d64df1ade..9ffaf6b60 100644
--- a/pom.xml
+++ b/pom.xml
@@ -68,6 +68,7 @@ under the License.
     <module>bom</module>
     <module>format</module>
     <module>memory</module>
+    <module>arrow-variant</module>
     <module>vector</module>
     <module>tools</module>
     <module>adapter/jdbc</module>
@@ -104,6 +105,7 @@ under the License.
     <dep.hadoop.version>3.4.2</dep.hadoop.version>
     <dep.fbs.version>25.2.10</dep.fbs.version>
     <dep.avro.version>1.12.1</dep.avro.version>
+    <dep.parquet.version>1.17.0</dep.parquet.version>
     <dep.mockito-bom.version>5.17.0</dep.mockito-bom.version>
     <arrow.vector.classifier></arrow.vector.classifier>
     <forkCount>2</forkCount>
diff --git a/vector/src/main/codegen/templates/AbstractFieldReader.java 
b/vector/src/main/codegen/templates/AbstractFieldReader.java
index 556fb576c..789295e95 100644
--- a/vector/src/main/codegen/templates/AbstractFieldReader.java
+++ b/vector/src/main/codegen/templates/AbstractFieldReader.java
@@ -29,9 +29,9 @@ package org.apache.arrow.vector.complex.impl;
  * Source code generated using FreeMarker template ${.template_name}
  */
 @SuppressWarnings("unused")
-abstract class AbstractFieldReader extends AbstractBaseReader implements 
FieldReader{
+public abstract class AbstractFieldReader extends AbstractBaseReader 
implements FieldReader{
 
-  AbstractFieldReader(){
+  protected AbstractFieldReader(){
     super();
   }
 

Reply via email to