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

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


The following commit(s) were added to refs/heads/main by this push:
     new b9d3b08a49 Use `Metadata` struct instead of `HashMap<String, String>` 
for metadata: Ordered;  cheap to clone (#10075)
b9d3b08a49 is described below

commit b9d3b08a49dbe13351cfe357c12eb03473267176
Author: Emil Ernerfeldt <[email protected]>
AuthorDate: Mon Aug 3 11:54:27 2026 -0700

    Use `Metadata` struct instead of `HashMap<String, String>` for metadata: 
Ordered;  cheap to clone (#10075)
    
    # Which issue does this PR close?
    
    - Closes #10069.
    
    # Rationale for this change
    
    `Field` and `Schema` store their metadata as `HashMap<String, String>`,
    which makes cloning them expensive (a deep clone with an allocation per
    entry). This is in contrast to basically everything else in `arrow-rs`,
    which uses `Arc` for cheap cloning. `HashMap` also has a
    non-deterministic iteration order, forcing every consumer that needs
    determinism (IPC encoding, `Display`, `Hash`, `Ord`) to collect and sort
    the keys first.
    
    # What changes are included in this PR?
    
    Adds a new `Metadata` type to `arrow-schema`:
    
    ```rust
    #[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
    pub struct Metadata(
        // `None` means empty, so an empty `Metadata` never allocates
        Option<Arc<BTreeMap<String, String>>>,
    );
    ```
    
    * Cloning is always cheap (bumps a refcount).
    * Mutation (`insert`/`remove`/`extend`) is copy-on-write via
    `Arc::make_mut`: the underlying map is deep-cloned only if it is shared.
    * Iteration order is deterministic (sorted by key) thanks to `BTreeMap`.
    * The inner map is never empty (`None` encodes the empty map), keeping
    the derived `PartialEq`/`Ord`/`Hash` implementations consistent.
    * Familiar map API: `get`, `insert`, `remove`, `contains_key`, `len`,
    `iter`, `keys`, `values`, `clear`, `Index<&str>`, plus `From`/`Into`
    conversions for `HashMap`, `BTreeMap`, and `[(K, V); N]` arrays, and
    `FromIterator`/`Extend`/`IntoIterator` implementations.
    * `serde` serialization format is identical to the `HashMap` it replaces
    (verified by a test asserting byte-for-byte equality with postcard).
    
    `Field::metadata`, `Schema::metadata`, and `SchemaBuilder::metadata` now
    use `Metadata`. The setters (`with_metadata`, `set_metadata`,
    `Schema::new_with_metadata`) take `impl Into<Metadata>`, so existing
    callers passing a `HashMap` keep compiling, and callers can now also
    write `field.with_metadata([("key", "value")])` directly.
    
    Now-redundant sorting has been removed, since `Metadata` already
    iterates in sorted key order:
    
    * `Field::cmp`, `Field::hash`, and `Schema::hash` (which now is derived)
    no longer collect and sort keys.
    * `arrow_ipc::convert::metadata_to_fb` no longer sorts keys (IPC,
    Flight, and Parquet schema encoding funnel through this).
    * The `DataType` `Display` impl no longer sorts metadata entries.
    
    # Are these changes tested?
    
    Yes:
    
    * New unit tests for `Metadata` covering the empty/allocation-free
    invariant, insert/get/remove, copy-on-write semantics, deterministic
    iteration order, equality with std maps, `Extend`, `Debug`, and serde
    round-trips (including format compatibility with the maps it replaces).
    * The existing test suites cover the integration (`cargo test
    --workspace --all-features` passes, apart from the pre-existing
    `test_shrink_to_fit_after_concat` failure that also fails on `main` with
    `--all-features`).
    
    # Are there any user-facing changes?
    
    Yes, this is a **breaking change** to `arrow-schema`:
    
    * `Field::metadata()`, `Field::metadata_mut()`, `Schema::metadata()`,
    `SchemaBuilder::metadata()`, and `SchemaBuilder::metadata_mut()` now
    return `&Metadata`/`&mut Metadata` instead of `&HashMap<String,
    String>`/`&mut HashMap<String, String>`. The public field
    `Schema::metadata` is now a `Metadata`.
    * `Field::with_metadata`, `Field::set_metadata`,
    `Schema::with_metadata`, and `Schema::new_with_metadata` now take `impl
    Into<Metadata>` instead of `HashMap<String, String>`. Callers passing a
    `HashMap` continue to compile, but callers relying on type inference
    (e.g. `.with_metadata(iter.collect())` or
    `.with_metadata(Default::default())`) need an explicit type annotation.
    * `ExtensionType::try_new_from_field_metadata` and
    `arrow_ipc::convert::metadata_to_fb` now take `&Metadata` instead of
    `&HashMap<String, String>`.
    * `RecordBatch::schema_metadata_mut` now returns `&mut Metadata`.
    * Code that needs an actual `HashMap`/`BTreeMap` can convert with
    `.into()` or iterate via `.iter()`.
    
    Since `Metadata` mirrors the map API, most code (e.g.
    `schema.metadata.get("key")`, `field.metadata().is_empty()`) compiles
    unchanged.
    
    Additionally, metadata iteration order (e.g. in `Debug` output) is now
    deterministic (sorted by key), where it previously followed `HashMap`'s
    arbitrary order.
    
    ---------
    
    Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
 arrow-array/src/record_batch.rs                    |   8 +-
 arrow-avro/src/reader/async_reader/mod.rs          |   8 +-
 arrow-avro/src/reader/mod.rs                       |  40 +-
 arrow-avro/src/schema.rs                           |  13 +-
 arrow-avro/src/writer/mod.rs                       |   4 +-
 arrow-csv/src/reader/mod.rs                        |   4 +-
 arrow-integration-test/src/lib.rs                  |  16 +-
 arrow-integration-test/src/schema.rs               |   7 +-
 arrow-ipc/src/convert.rs                           |  12 +-
 arrow-ipc/src/writer.rs                            |   4 +-
 arrow-schema/src/datatype_display.rs               |  15 +-
 arrow-schema/src/extension/canonical/bool8.rs      |  29 +-
 .../src/extension/canonical/fixed_shape_tensor.rs  |  39 +-
 arrow-schema/src/extension/canonical/json.rs       |  26 +-
 arrow-schema/src/extension/canonical/opaque.rs     |  37 +-
 .../extension/canonical/timestamp_with_offset.rs   |  25 +-
 arrow-schema/src/extension/canonical/uuid.rs       |  27 +-
 .../extension/canonical/variable_shape_tensor.rs   |  36 +-
 arrow-schema/src/extension/mod.rs                  |   3 +-
 arrow-schema/src/ffi.rs                            |   2 +-
 arrow-schema/src/field.rs                          | 107 ++---
 arrow-schema/src/lib.rs                            |   2 +
 arrow-schema/src/metadata.rs                       | 473 +++++++++++++++++++++
 arrow-schema/src/schema.rs                         | 108 ++---
 arrow/src/datatypes/mod.rs                         |   4 +-
 parquet/src/arrow/arrow_reader/mod.rs              |   4 +-
 parquet/src/arrow/arrow_writer/mod.rs              |   6 +-
 parquet/src/arrow/schema/complex.rs                |   7 +-
 parquet/src/arrow/schema/virtual_type.rs           |  70 +--
 29 files changed, 667 insertions(+), 469 deletions(-)

diff --git a/arrow-array/src/record_batch.rs b/arrow-array/src/record_batch.rs
index e05450a97f..911604cd0b 100644
--- a/arrow-array/src/record_batch.rs
+++ b/arrow-array/src/record_batch.rs
@@ -443,10 +443,10 @@ impl RecordBatch {
     /// // Initially, the metadata is empty
     /// assert!(batch.schema().metadata().get("key").is_none());
     /// // Insert a key-value pair into the metadata
-    /// batch.schema_metadata_mut().insert("key".into(), "value".into());
+    /// batch.schema_metadata_mut().insert("key", "value");
     /// assert_eq!(batch.schema().metadata().get("key"), 
Some(&String::from("value")));
     /// ```
-    pub fn schema_metadata_mut(&mut self) -> &mut 
std::collections::HashMap<String, String> {
+    pub fn schema_metadata_mut(&mut self) -> &mut arrow_schema::Metadata {
         let schema = Arc::make_mut(&mut self.schema);
         &mut schema.metadata
     }
@@ -1709,9 +1709,7 @@ mod tests {
         batch.clone().with_schema(required_schema).unwrap_err();
 
         // Can add metadata
-        let metadata = vec![("foo".to_string(), "bar".to_string())]
-            .into_iter()
-            .collect();
+        let metadata = arrow_schema::Metadata::from([("foo", "bar")]);
         let metadata_schema = 
nullable_schema.as_ref().clone().with_metadata(metadata);
         let batch = batch.with_schema(Arc::new(metadata_schema)).unwrap();
 
diff --git a/arrow-avro/src/reader/async_reader/mod.rs 
b/arrow-avro/src/reader/async_reader/mod.rs
index 0d4603e762..84ba1c873b 100644
--- a/arrow-avro/src/reader/async_reader/mod.rs
+++ b/arrow-avro/src/reader/async_reader/mod.rs
@@ -556,7 +556,7 @@ mod tests {
     use arrow_array::cast::AsArray;
     use arrow_array::types::{Int32Type, Int64Type};
     use arrow_array::*;
-    use arrow_schema::{DataType, Field, Schema, SchemaRef, TimeUnit};
+    use arrow_schema::{DataType, Field, Metadata, Schema, SchemaRef, TimeUnit};
     use futures::{StreamExt, TryStreamExt};
     use object_store::local::LocalFileSystem;
     use object_store::path::Path;
@@ -1729,7 +1729,7 @@ mod tests {
         let expected_schema = get_alltypes_schema()
             .as_ref()
             .clone()
-            .with_metadata(Default::default());
+            .with_metadata(Metadata::default());
 
         // Build reader without providing reader schema - should use writer 
schema from file
         let reader = AsyncAvroFileReader::builder(file_reader, file_size, 1024)
@@ -1756,7 +1756,7 @@ mod tests {
         let schema = get_alltypes_schema()
             .project(&[0, 1, 7])
             .unwrap()
-            .with_metadata(Default::default());
+            .with_metadata(Metadata::default());
         let reader_schema = AvroSchema::try_from(&schema).unwrap();
         let expected_schema = schema.clone();
 
@@ -1789,7 +1789,7 @@ mod tests {
         let expected_schema = get_nested_records_schema()
             .as_ref()
             .clone()
-            .with_metadata(Default::default());
+            .with_metadata(Metadata::default());
 
         let reader = AsyncAvroFileReader::builder(file_reader, file_size, 1024)
             .try_build()
diff --git a/arrow-avro/src/reader/mod.rs b/arrow-avro/src/reader/mod.rs
index cee51a89e5..1b966060e4 100644
--- a/arrow-avro/src/reader/mod.rs
+++ b/arrow-avro/src/reader/mod.rs
@@ -5483,34 +5483,14 @@ mod test {
             #[cfg(not(feature = "avro_custom_types"))]
             {
                 let schema = Arc::new(Schema::new(vec![
-                    Field::new("duration_time_nanos", DataType::Int64, 
false).with_metadata(
-                        [(
-                            "logicalType".to_string(),
-                            "arrow.duration-nanos".to_string(),
-                        )]
-                        .into(),
-                    ),
-                    Field::new("duration_time_micros", DataType::Int64, 
false).with_metadata(
-                        [(
-                            "logicalType".to_string(),
-                            "arrow.duration-micros".to_string(),
-                        )]
-                        .into(),
-                    ),
-                    Field::new("duration_time_millis", DataType::Int64, 
false).with_metadata(
-                        [(
-                            "logicalType".to_string(),
-                            "arrow.duration-millis".to_string(),
-                        )]
-                        .into(),
-                    ),
-                    Field::new("duration_time_seconds", DataType::Int64, 
false).with_metadata(
-                        [(
-                            "logicalType".to_string(),
-                            "arrow.duration-seconds".to_string(),
-                        )]
-                        .into(),
-                    ),
+                    Field::new("duration_time_nanos", DataType::Int64, false)
+                        .with_metadata([("logicalType", 
"arrow.duration-nanos")]),
+                    Field::new("duration_time_micros", DataType::Int64, false)
+                        .with_metadata([("logicalType", 
"arrow.duration-micros")]),
+                    Field::new("duration_time_millis", DataType::Int64, false)
+                        .with_metadata([("logicalType", 
"arrow.duration-millis")]),
+                    Field::new("duration_time_seconds", DataType::Int64, false)
+                        .with_metadata([("logicalType", 
"arrow.duration-seconds")]),
                 ]));
 
                 let nanos =
@@ -8426,7 +8406,7 @@ mod test {
         const UUID_EXT_KEY: &str = "ARROW:extension:name";
         const UUID_LOGICAL_KEY: &str = "logicalType";
 
-        let uuid_md_top: Option<HashMap<String, String>> = batch
+        let uuid_md_top: Option<arrow_schema::Metadata> = batch
             .schema()
             .field_with_name("uuid_str")
             .ok()
@@ -8444,7 +8424,7 @@ mod test {
                 }
             });
 
-        let uuid_md_union: Option<HashMap<String, String>> = batch
+        let uuid_md_union: Option<arrow_schema::Metadata> = batch
             .schema()
             .field_with_name("union_uuid_or_fixed10")
             .ok()
diff --git a/arrow-avro/src/schema.rs b/arrow-avro/src/schema.rs
index 1b0c2e26f7..28965e5e99 100644
--- a/arrow-avro/src/schema.rs
+++ b/arrow-avro/src/schema.rs
@@ -20,8 +20,8 @@
 #[cfg(feature = "canonical_extension_types")]
 use arrow_schema::extension::ExtensionType;
 use arrow_schema::{
-    ArrowError, DataType, Field as ArrowField, IntervalUnit, Schema as 
ArrowSchema, TimeUnit,
-    UnionMode,
+    ArrowError, DataType, Field as ArrowField, IntervalUnit, Metadata, Schema 
as ArrowSchema,
+    TimeUnit, UnionMode,
 };
 use serde::{Deserialize, Serialize};
 use serde_json::{Map as JsonMap, Value, json};
@@ -1155,10 +1155,7 @@ fn is_internal_arrow_key(key: &str) -> bool {
 /// skipping keys that are Avro-reserved, internal Arrow keys, or
 /// nested under the `avro.schema.` namespace. Values that parse as
 /// JSON are inserted as JSON; otherwise the raw string is preserved.
-fn extend_with_passthrough_metadata(
-    target: &mut JsonMap<String, Value>,
-    metadata: &HashMap<String, String>,
-) {
+fn extend_with_passthrough_metadata(target: &mut JsonMap<String, Value>, 
metadata: &Metadata) {
     for (meta_key, meta_val) in metadata {
         if meta_key.starts_with("avro.") || is_internal_arrow_key(meta_key) {
             continue;
@@ -1318,7 +1315,7 @@ fn union_branch_signature(branch: &Value) -> 
Result<String, ArrowError> {
 fn datatype_to_avro(
     dt: &DataType,
     field_name: &str,
-    metadata: &HashMap<String, String>,
+    metadata: &Metadata,
     name_gen: &mut NameGenerator,
     null_order: Nullability,
     strip: bool,
@@ -1915,7 +1912,7 @@ fn datatype_to_avro(
 fn process_datatype(
     dt: &DataType,
     field_name: &str,
-    metadata: &HashMap<String, String>,
+    metadata: &Metadata,
     name_gen: &mut NameGenerator,
     null_order: Nullability,
     is_nullable: bool,
diff --git a/arrow-avro/src/writer/mod.rs b/arrow-avro/src/writer/mod.rs
index 8d078b1b82..235db3f0f7 100644
--- a/arrow-avro/src/writer/mod.rs
+++ b/arrow-avro/src/writer/mod.rs
@@ -1883,8 +1883,8 @@ mod tests {
     /// Checks that `actual_meta` contains all of `expected_meta`, and any 
additional
     /// keys in `actual_meta` are from a permitted set.
     fn assert_metadata_is_superset(
-        expected_meta: &HashMap<String, String>,
-        actual_meta: &HashMap<String, String>,
+        expected_meta: &arrow_schema::Metadata,
+        actual_meta: &arrow_schema::Metadata,
         context: &str,
     ) {
         let allowed_additions: HashSet<&str> =
diff --git a/arrow-csv/src/reader/mod.rs b/arrow-csv/src/reader/mod.rs
index 51e855f7df..4d56d7e991 100644
--- a/arrow-csv/src/reader/mod.rs
+++ b/arrow-csv/src/reader/mod.rs
@@ -728,7 +728,7 @@ fn validate_header(rows: &StringRecords<'_>, fields: 
&Fields) -> Result<(), Arro
 fn parse(
     rows: &StringRecords<'_>,
     fields: &Fields,
-    metadata: Option<std::collections::HashMap<String, String>>,
+    metadata: Option<Metadata>,
     projection: Option<&Vec<usize>>,
     line_number: usize,
     null_regex: &NullRegex,
@@ -1384,7 +1384,7 @@ mod tests {
         assert_eq!(37, batch.num_rows());
         assert_eq!(3, batch.num_columns());
 
-        assert_eq!(&metadata, batch.schema().metadata());
+        assert_eq!(batch.schema().metadata(), &metadata);
     }
 
     #[test]
diff --git a/arrow-integration-test/src/lib.rs 
b/arrow-integration-test/src/lib.rs
index e0aa3ecf85..ac027554b2 100644
--- a/arrow-integration-test/src/lib.rs
+++ b/arrow-integration-test/src/lib.rs
@@ -1341,18 +1341,10 @@ mod tests {
         let nanos_tz = Some("Africa/Johannesburg".into());
 
         let schema = Schema::new(vec![
-            Field::new("bools-with-metadata-map", DataType::Boolean, 
true).with_metadata(
-                [("k".to_string(), "v".to_string())]
-                    .iter()
-                    .cloned()
-                    .collect(),
-            ),
-            Field::new("bools-with-metadata-vec", DataType::Boolean, 
true).with_metadata(
-                [("k2".to_string(), "v2".to_string())]
-                    .iter()
-                    .cloned()
-                    .collect(),
-            ),
+            Field::new("bools-with-metadata-map", DataType::Boolean, true)
+                .with_metadata([("k", "v")]),
+            Field::new("bools-with-metadata-vec", DataType::Boolean, true)
+                .with_metadata([("k2", "v2")]),
             Field::new("bools", DataType::Boolean, true),
             Field::new("int8s", DataType::Int8, true),
             Field::new("int16s", DataType::Int16, true),
diff --git a/arrow-integration-test/src/schema.rs 
b/arrow-integration-test/src/schema.rs
index 7777c48c1f..8e0dd730d9 100644
--- a/arrow-integration-test/src/schema.rs
+++ b/arrow-integration-test/src/schema.rs
@@ -22,9 +22,14 @@ use std::collections::HashMap;
 
 /// Generate a JSON representation of the `Schema`.
 pub fn schema_to_json(schema: &Schema) -> serde_json::Value {
+    let metadata: serde_json::Map<String, serde_json::Value> = schema
+        .metadata()
+        .iter()
+        .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
+        .collect();
     serde_json::json!({
         "fields": schema.fields().iter().map(|f| 
field_to_json(f.as_ref())).collect::<Vec<_>>(),
-        "metadata": serde_json::to_value(schema.metadata()).unwrap()
+        "metadata": metadata
     })
 }
 
diff --git a/arrow-ipc/src/convert.rs b/arrow-ipc/src/convert.rs
index 16e61deadb..5b65c81a94 100644
--- a/arrow-ipc/src/convert.rs
+++ b/arrow-ipc/src/convert.rs
@@ -131,14 +131,12 @@ impl<'a> IpcSchemaEncoder<'a> {
 /// Push a key-value metadata into a FlatBufferBuilder and return [WIPOffset]
 pub fn metadata_to_fb<'a>(
     fbb: &mut FlatBufferBuilder<'a>,
-    metadata: &HashMap<String, String>,
+    metadata: &Metadata,
 ) -> WIPOffset<Vector<'a, ForwardsUOffset<KeyValue<'a>>>> {
-    let mut ordered_keys = metadata.keys().collect::<Vec<_>>();
-    ordered_keys.sort();
-    let custom_metadata = ordered_keys
-        .into_iter()
-        .map(|k| {
-            let v = metadata.get(k).unwrap();
+    // `Metadata` iterates in deterministic (sorted) key order
+    let custom_metadata = metadata
+        .iter()
+        .map(|(k, v)| {
             let fb_key_name = fbb.create_string(k);
             let fb_val_name = fbb.create_string(v);
 
diff --git a/arrow-ipc/src/writer.rs b/arrow-ipc/src/writer.rs
index 73994fad8e..4b46b7c790 100644
--- a/arrow-ipc/src/writer.rs
+++ b/arrow-ipc/src/writer.rs
@@ -1608,7 +1608,7 @@ pub struct FileWriter<W> {
     /// Keeps track of dictionaries that have been written
     dictionary_tracker: DictionaryTracker,
     /// User level customized metadata
-    custom_metadata: HashMap<String, String>,
+    custom_metadata: Metadata,
 
     data_gen: IpcDataGenerator,
 
@@ -1674,7 +1674,7 @@ impl<W: Write> FileWriter<W> {
             record_blocks: vec![],
             finished: false,
             dictionary_tracker,
-            custom_metadata: HashMap::new(),
+            custom_metadata: Default::default(),
             data_gen,
             ipc_write_context: IpcWriteContext::default(),
         })
diff --git a/arrow-schema/src/datatype_display.rs 
b/arrow-schema/src/datatype_display.rs
index cca7cf254f..203d4a1584 100644
--- a/arrow-schema/src/datatype_display.rs
+++ b/arrow-schema/src/datatype_display.rs
@@ -16,12 +16,13 @@
 // under the License.
 
 use crate::DataType;
+use crate::Metadata;
+use std::fmt;
 use std::fmt::Display;
-use std::{collections::HashMap, fmt};
 
 impl Display for DataType {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        fn format_metadata(metadata: &HashMap<String, String>) -> String {
+        fn format_metadata(metadata: &Metadata) -> String {
             format!("{}", FormatMetadata(metadata))
         }
 
@@ -183,8 +184,8 @@ impl Display for DataType {
     }
 }
 
-/// Adapter to format a metadata HashMap consistently.
-struct FormatMetadata<'a>(&'a HashMap<String, String>);
+/// Adapter to format [`Metadata`] consistently.
+struct FormatMetadata<'a>(&'a Metadata);
 
 impl fmt::Display for FormatMetadata<'_> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -192,10 +193,9 @@ impl fmt::Display for FormatMetadata<'_> {
         if metadata.is_empty() {
             Ok(())
         } else {
-            let mut entries: Vec<(&String, &String)> = 
metadata.iter().collect();
-            entries.sort_by(|a, b| a.0.cmp(b.0));
+            // `Metadata` iterates in sorted key order
             write!(f, ", metadata: ")?;
-            f.debug_map().entries(entries).finish()
+            f.debug_map().entries(metadata.iter()).finish()
         }
     }
 }
@@ -203,6 +203,7 @@ impl fmt::Display for FormatMetadata<'_> {
 #[cfg(test)]
 mod tests {
 
+    use std::collections::HashMap;
     use std::sync::Arc;
 
     use crate::Field;
diff --git a/arrow-schema/src/extension/canonical/bool8.rs 
b/arrow-schema/src/extension/canonical/bool8.rs
index 95482bc540..17eeb240ac 100644
--- a/arrow-schema/src/extension/canonical/bool8.rs
+++ b/arrow-schema/src/extension/canonical/bool8.rs
@@ -102,11 +102,8 @@ mod tests {
     #[test]
     #[should_panic(expected = "Extension type name missing")]
     fn missing_name() {
-        let field = Field::new("", DataType::Int8, false).with_metadata(
-            [(EXTENSION_TYPE_METADATA_KEY.to_owned(), "".to_owned())]
-                .into_iter()
-                .collect(),
-        );
+        let field = Field::new("", DataType::Int8, false)
+            .with_metadata([(EXTENSION_TYPE_METADATA_KEY, "")]);
         field.extension_type::<Bool8>();
     }
 
@@ -119,28 +116,18 @@ mod tests {
     #[test]
     #[should_panic(expected = "Bool8 extension type expects an empty string as 
metadata")]
     fn missing_metadata() {
-        let field = Field::new("", DataType::Int8, false).with_metadata(
-            [(EXTENSION_TYPE_NAME_KEY.to_owned(), Bool8::NAME.to_owned())]
-                .into_iter()
-                .collect(),
-        );
+        let field = Field::new("", DataType::Int8, false)
+            .with_metadata([(EXTENSION_TYPE_NAME_KEY, Bool8::NAME)]);
         field.extension_type::<Bool8>();
     }
 
     #[test]
     #[should_panic(expected = "Bool8 extension type expects an empty string as 
metadata")]
     fn invalid_metadata() {
-        let field = Field::new("", DataType::Int8, false).with_metadata(
-            [
-                (EXTENSION_TYPE_NAME_KEY.to_owned(), Bool8::NAME.to_owned()),
-                (
-                    EXTENSION_TYPE_METADATA_KEY.to_owned(),
-                    "non-empty".to_owned(),
-                ),
-            ]
-            .into_iter()
-            .collect(),
-        );
+        let field = Field::new("", DataType::Int8, false).with_metadata([
+            (EXTENSION_TYPE_NAME_KEY, Bool8::NAME),
+            (EXTENSION_TYPE_METADATA_KEY, "non-empty"),
+        ]);
         field.extension_type::<Bool8>();
     }
 }
diff --git a/arrow-schema/src/extension/canonical/fixed_shape_tensor.rs 
b/arrow-schema/src/extension/canonical/fixed_shape_tensor.rs
index 5157eefe9e..00c8564350 100644
--- a/arrow-schema/src/extension/canonical/fixed_shape_tensor.rs
+++ b/arrow-schema/src/extension/canonical/fixed_shape_tensor.rs
@@ -475,14 +475,10 @@ mod tests {
     fn missing_name() {
         let field =
             Field::new_fixed_size_list("", Field::new("", DataType::Float32, 
false), 3, false)
-                .with_metadata(
-                    [(
-                        EXTENSION_TYPE_METADATA_KEY.to_owned(),
-                        r#"{ "shape": [100, 200, 500], }"#.to_owned(),
-                    )]
-                    .into_iter()
-                    .collect(),
-                );
+                .with_metadata([(
+                    EXTENSION_TYPE_METADATA_KEY,
+                    r#"{ "shape": [100, 200, 500], }"#,
+                )]);
         field.extension_type::<FixedShapeTensor>();
     }
 
@@ -505,14 +501,7 @@ mod tests {
     fn missing_metadata() {
         let field =
             Field::new_fixed_size_list("", Field::new("", DataType::Float32, 
false), 3, false)
-                .with_metadata(
-                    [(
-                        EXTENSION_TYPE_NAME_KEY.to_owned(),
-                        FixedShapeTensor::NAME.to_owned(),
-                    )]
-                    .into_iter()
-                    .collect(),
-                );
+                .with_metadata([(EXTENSION_TYPE_NAME_KEY, 
FixedShapeTensor::NAME)]);
         field.extension_type::<FixedShapeTensor>();
     }
 
@@ -528,20 +517,10 @@ mod tests {
             i32::try_from(fixed_shape_tensor.list_size()).expect("overflow"),
             false,
         )
-        .with_metadata(
-            [
-                (
-                    EXTENSION_TYPE_NAME_KEY.to_owned(),
-                    FixedShapeTensor::NAME.to_owned(),
-                ),
-                (
-                    EXTENSION_TYPE_METADATA_KEY.to_owned(),
-                    r#"{ "not-shape": [] }"#.to_owned(),
-                ),
-            ]
-            .into_iter()
-            .collect(),
-        );
+        .with_metadata([
+            (EXTENSION_TYPE_NAME_KEY, FixedShapeTensor::NAME),
+            (EXTENSION_TYPE_METADATA_KEY, r#"{ "not-shape": [] }"#),
+        ]);
         field.extension_type::<FixedShapeTensor>();
     }
 
diff --git a/arrow-schema/src/extension/canonical/json.rs 
b/arrow-schema/src/extension/canonical/json.rs
index 99b78fd6ef..1188a6dda4 100644
--- a/arrow-schema/src/extension/canonical/json.rs
+++ b/arrow-schema/src/extension/canonical/json.rs
@@ -228,11 +228,8 @@ mod tests {
     #[test]
     #[should_panic(expected = "Extension type name missing")]
     fn missing_name() {
-        let field = Field::new("", DataType::Int8, false).with_metadata(
-            [(EXTENSION_TYPE_METADATA_KEY.to_owned(), "{}".to_owned())]
-                .into_iter()
-                .collect(),
-        );
+        let field = Field::new("", DataType::Int8, false)
+            .with_metadata([(EXTENSION_TYPE_METADATA_KEY, "{}")]);
         field.extension_type::<Json>();
     }
 
@@ -247,14 +244,10 @@ mod tests {
         expected = "Json extension type metadata is either an empty string or 
a JSON string with an empty object"
     )]
     fn invalid_metadata() {
-        let field = Field::new("", DataType::Utf8, false).with_metadata(
-            [
-                (EXTENSION_TYPE_NAME_KEY.to_owned(), Json::NAME.to_owned()),
-                (EXTENSION_TYPE_METADATA_KEY.to_owned(), "1234".to_owned()),
-            ]
-            .into_iter()
-            .collect(),
-        );
+        let field = Field::new("", DataType::Utf8, false).with_metadata([
+            (EXTENSION_TYPE_NAME_KEY, Json::NAME),
+            (EXTENSION_TYPE_METADATA_KEY, "1234"),
+        ]);
         field.extension_type::<Json>();
     }
 
@@ -263,11 +256,8 @@ mod tests {
         expected = "Json extension type metadata is either an empty string or 
a JSON string with an empty object"
     )]
     fn missing_metadata() {
-        let field = Field::new("", DataType::LargeUtf8, false).with_metadata(
-            [(EXTENSION_TYPE_NAME_KEY.to_owned(), Json::NAME.to_owned())]
-                .into_iter()
-                .collect(),
-        );
+        let field = Field::new("", DataType::LargeUtf8, false)
+            .with_metadata([(EXTENSION_TYPE_NAME_KEY, Json::NAME)]);
         field.extension_type::<Json>();
     }
 }
diff --git a/arrow-schema/src/extension/canonical/opaque.rs 
b/arrow-schema/src/extension/canonical/opaque.rs
index 5a5eb1b668..62ae062ca1 100644
--- a/arrow-schema/src/extension/canonical/opaque.rs
+++ b/arrow-schema/src/extension/canonical/opaque.rs
@@ -291,25 +291,18 @@ mod tests {
     #[test]
     #[should_panic(expected = "Extension type name missing")]
     fn missing_name() {
-        let field = Field::new("", DataType::Null, false).with_metadata(
-            [(
-                EXTENSION_TYPE_METADATA_KEY.to_owned(),
-                r#"{ "type_name": "type", "vendor_name": "vendor" 
}"#.to_owned(),
-            )]
-            .into_iter()
-            .collect(),
-        );
+        let field = Field::new("", DataType::Null, false).with_metadata([(
+            EXTENSION_TYPE_METADATA_KEY,
+            r#"{ "type_name": "type", "vendor_name": "vendor" }"#,
+        )]);
         field.extension_type::<Opaque>();
     }
 
     #[test]
     #[should_panic(expected = "Opaque extension types requires metadata")]
     fn missing_metadata() {
-        let field = Field::new("", DataType::Null, false).with_metadata(
-            [(EXTENSION_TYPE_NAME_KEY.to_owned(), Opaque::NAME.to_owned())]
-                .into_iter()
-                .collect(),
-        );
+        let field = Field::new("", DataType::Null, false)
+            .with_metadata([(EXTENSION_TYPE_NAME_KEY, Opaque::NAME)]);
         field.extension_type::<Opaque>();
     }
 
@@ -318,17 +311,13 @@ mod tests {
         expected = "Opaque metadata deserialization failed: missing field 
`vendor_name`"
     )]
     fn invalid_metadata() {
-        let field = Field::new("", DataType::Null, false).with_metadata(
-            [
-                (EXTENSION_TYPE_NAME_KEY.to_owned(), Opaque::NAME.to_owned()),
-                (
-                    EXTENSION_TYPE_METADATA_KEY.to_owned(),
-                    r#"{ "type_name": "no-vendor" }"#.to_owned(),
-                ),
-            ]
-            .into_iter()
-            .collect(),
-        );
+        let field = Field::new("", DataType::Null, false).with_metadata([
+            (EXTENSION_TYPE_NAME_KEY, Opaque::NAME),
+            (
+                EXTENSION_TYPE_METADATA_KEY,
+                r#"{ "type_name": "no-vendor" }"#,
+            ),
+        ]);
         field.extension_type::<Opaque>();
     }
 }
diff --git a/arrow-schema/src/extension/canonical/timestamp_with_offset.rs 
b/arrow-schema/src/extension/canonical/timestamp_with_offset.rs
index 06ea98b3a0..84b49564ae 100644
--- a/arrow-schema/src/extension/canonical/timestamp_with_offset.rs
+++ b/arrow-schema/src/extension/canonical/timestamp_with_offset.rs
@@ -307,7 +307,7 @@ mod tests {
     #[should_panic(expected = "Extension type name missing")]
     fn missing_name() {
         let field = make_valid_field_primitive(TimeUnit::Second)
-            .with_metadata([(EXTENSION_TYPE_METADATA_KEY.to_owned(), 
"".to_owned())].into());
+            .with_metadata([(EXTENSION_TYPE_METADATA_KEY, "")]);
         field.extension_type::<TimestampWithOffset>();
     }
 
@@ -509,28 +509,17 @@ mod tests {
 
     #[test]
     fn no_metadata() {
-        let field = make_valid_field_primitive(TimeUnit::Second).with_metadata(
-            [(
-                EXTENSION_TYPE_NAME_KEY.to_owned(),
-                TimestampWithOffset::NAME.to_owned(),
-            )]
-            .into(),
-        );
+        let field = make_valid_field_primitive(TimeUnit::Second)
+            .with_metadata([(EXTENSION_TYPE_NAME_KEY, 
TimestampWithOffset::NAME)]);
         field.extension_type::<TimestampWithOffset>();
     }
 
     #[test]
     fn empty_metadata() {
-        let field = make_valid_field_primitive(TimeUnit::Second).with_metadata(
-            [
-                (
-                    EXTENSION_TYPE_NAME_KEY.to_owned(),
-                    TimestampWithOffset::NAME.to_owned(),
-                ),
-                (EXTENSION_TYPE_METADATA_KEY.to_owned(), String::new()),
-            ]
-            .into(),
-        );
+        let field = 
make_valid_field_primitive(TimeUnit::Second).with_metadata([
+            (EXTENSION_TYPE_NAME_KEY, TimestampWithOffset::NAME),
+            (EXTENSION_TYPE_METADATA_KEY, ""),
+        ]);
         field.extension_type::<TimestampWithOffset>();
     }
 }
diff --git a/arrow-schema/src/extension/canonical/uuid.rs 
b/arrow-schema/src/extension/canonical/uuid.rs
index 7a9f6e6e53..16e7de4380 100644
--- a/arrow-schema/src/extension/canonical/uuid.rs
+++ b/arrow-schema/src/extension/canonical/uuid.rs
@@ -123,30 +123,19 @@ mod tests {
     #[test]
     #[should_panic(expected = "Uuid extension type expects no metadata")]
     fn with_metadata() {
-        let field = Field::new("", DataType::FixedSizeBinary(16), 
false).with_metadata(
-            [
-                (EXTENSION_TYPE_NAME_KEY.to_owned(), Uuid::NAME.to_owned()),
-                (
-                    EXTENSION_TYPE_METADATA_KEY.to_owned(),
-                    "unexpected".to_owned(),
-                ),
-            ]
-            .into_iter()
-            .collect(),
-        );
+        let field = Field::new("", DataType::FixedSizeBinary(16), 
false).with_metadata([
+            (EXTENSION_TYPE_NAME_KEY, Uuid::NAME),
+            (EXTENSION_TYPE_METADATA_KEY, "unexpected"),
+        ]);
         field.extension_type::<Uuid>();
     }
 
     #[test]
     fn empty_metadata_string_is_treated_as_none() -> Result<(), ArrowError> {
-        let field = Field::new("", DataType::FixedSizeBinary(16), 
false).with_metadata(
-            [
-                (EXTENSION_TYPE_NAME_KEY.to_owned(), Uuid::NAME.to_owned()),
-                (EXTENSION_TYPE_METADATA_KEY.to_owned(), "".to_owned()),
-            ]
-            .into_iter()
-            .collect(),
-        );
+        let field = Field::new("", DataType::FixedSizeBinary(16), 
false).with_metadata([
+            (EXTENSION_TYPE_NAME_KEY, Uuid::NAME),
+            (EXTENSION_TYPE_METADATA_KEY, ""),
+        ]);
         field.try_extension_type::<Uuid>()?;
         Ok(())
     }
diff --git a/arrow-schema/src/extension/canonical/variable_shape_tensor.rs 
b/arrow-schema/src/extension/canonical/variable_shape_tensor.rs
index fbc641f543..762790a379 100644
--- a/arrow-schema/src/extension/canonical/variable_shape_tensor.rs
+++ b/arrow-schema/src/extension/canonical/variable_shape_tensor.rs
@@ -548,11 +548,7 @@ mod tests {
             ],
             false,
         )
-        .with_metadata(
-            [(EXTENSION_TYPE_METADATA_KEY.to_owned(), "{}".to_owned())]
-                .into_iter()
-                .collect(),
-        );
+        .with_metadata([(EXTENSION_TYPE_METADATA_KEY, "{}")]);
         field.extension_type::<VariableShapeTensor>();
     }
 
@@ -601,14 +597,7 @@ mod tests {
             ],
             false,
         )
-        .with_metadata(
-            [(
-                EXTENSION_TYPE_NAME_KEY.to_owned(),
-                VariableShapeTensor::NAME.to_owned(),
-            )]
-            .into_iter()
-            .collect(),
-        );
+        .with_metadata([(EXTENSION_TYPE_NAME_KEY, VariableShapeTensor::NAME)]);
         field.extension_type::<VariableShapeTensor>();
     }
 
@@ -632,20 +621,13 @@ mod tests {
             ],
             false,
         )
-        .with_metadata(
-            [
-                (
-                    EXTENSION_TYPE_NAME_KEY.to_owned(),
-                    VariableShapeTensor::NAME.to_owned(),
-                ),
-                (
-                    EXTENSION_TYPE_METADATA_KEY.to_owned(),
-                    r#"{ "dim_names": [1, null, 3, 4] }"#.to_owned(),
-                ),
-            ]
-            .into_iter()
-            .collect(),
-        );
+        .with_metadata([
+            (EXTENSION_TYPE_NAME_KEY, VariableShapeTensor::NAME),
+            (
+                EXTENSION_TYPE_METADATA_KEY,
+                r#"{ "dim_names": [1, null, 3, 4] }"#,
+            ),
+        ]);
         field.extension_type::<VariableShapeTensor>();
     }
 
diff --git a/arrow-schema/src/extension/mod.rs 
b/arrow-schema/src/extension/mod.rs
index 3dd1f8d354..58bbb70497 100644
--- a/arrow-schema/src/extension/mod.rs
+++ b/arrow-schema/src/extension/mod.rs
@@ -23,7 +23,6 @@ mod canonical;
 pub use canonical::*;
 
 use crate::{ArrowError, DataType};
-use std::collections::HashMap;
 
 /// The metadata key for the string name identifying an [`ExtensionType`].
 pub const EXTENSION_TYPE_NAME_KEY: &str = "ARROW:extension:name";
@@ -283,7 +282,7 @@ pub trait ExtensionType: Sized {
     /// [`Field`]: crate::Field
     fn try_new_from_field_metadata(
         data_type: &DataType,
-        metadata: &HashMap<String, String>,
+        metadata: &crate::Metadata,
     ) -> Result<Self, ArrowError> {
         // Check the extension name in the metadata
         match metadata.get(EXTENSION_TYPE_NAME_KEY).map(|s| s.as_str()) {
diff --git a/arrow-schema/src/ffi.rs b/arrow-schema/src/ffi.rs
index 0ce819e35e..f377ffc7ef 100644
--- a/arrow-schema/src/ffi.rs
+++ b/arrow-schema/src/ffi.rs
@@ -937,7 +937,7 @@ mod tests {
             Field::new("address", DataType::Utf8, false),
             Field::new("priority", DataType::UInt8, false),
         ])
-        .with_metadata([("hello".to_string(), "world".to_string())].into());
+        .with_metadata([("hello", "world")]);
 
         round_trip_schema(schema);
 
diff --git a/arrow-schema/src/field.rs b/arrow-schema/src/field.rs
index 0d12728ca2..18d35d4c7c 100644
--- a/arrow-schema/src/field.rs
+++ b/arrow-schema/src/field.rs
@@ -17,7 +17,6 @@
 
 use crate::error::ArrowError;
 use std::cmp::Ordering;
-use std::collections::HashMap;
 use std::hash::{Hash, Hasher};
 use std::sync::Arc;
 
@@ -26,7 +25,7 @@ use crate::datatype::DataType;
 use crate::extension::CanonicalExtensionType;
 use crate::schema::SchemaBuilder;
 use crate::{
-    Fields, UnionFields, UnionMode,
+    Fields, Metadata, UnionFields, UnionMode,
     extension::{EXTENSION_TYPE_METADATA_KEY, EXTENSION_TYPE_NAME_KEY, 
ExtensionType},
 };
 
@@ -57,7 +56,7 @@ pub struct Field {
     dict_id: i64,
     dict_is_ordered: bool,
     /// A map of key-value pairs containing additional custom meta data.
-    metadata: HashMap<String, String>,
+    metadata: Metadata,
 }
 
 impl std::fmt::Debug for Field {
@@ -128,31 +127,7 @@ impl Ord for Field {
             .cmp(other.name())
             .then_with(|| self.data_type.cmp(other.data_type()))
             .then_with(|| self.nullable.cmp(&other.nullable))
-            .then_with(|| {
-                // ensure deterministic key order
-                let mut keys: Vec<&String> =
-                    
self.metadata.keys().chain(other.metadata.keys()).collect();
-                keys.sort();
-                for k in keys {
-                    match (self.metadata.get(k), other.metadata.get(k)) {
-                        (None, None) => {}
-                        (Some(_), None) => {
-                            return Ordering::Less;
-                        }
-                        (None, Some(_)) => {
-                            return Ordering::Greater;
-                        }
-                        (Some(v1), Some(v2)) => match v1.cmp(v2) {
-                            Ordering::Equal => {}
-                            other => {
-                                return other;
-                            }
-                        },
-                    }
-                }
-
-                Ordering::Equal
-            })
+            .then_with(|| self.metadata.cmp(&other.metadata))
     }
 }
 
@@ -161,14 +136,8 @@ impl Hash for Field {
         self.name.hash(state);
         self.data_type.hash(state);
         self.nullable.hash(state);
-
-        // ensure deterministic key order
-        let mut keys: Vec<&String> = self.metadata.keys().collect();
-        keys.sort();
-        for k in keys {
-            k.hash(state);
-            self.metadata.get(k).expect("key valid").hash(state);
-        }
+        // `Metadata` iterates in deterministic (sorted) key order
+        self.metadata.hash(state);
     }
 }
 
@@ -197,7 +166,7 @@ impl Field {
             nullable,
             dict_id: 0,
             dict_is_ordered: false,
-            metadata: HashMap::default(),
+            metadata: Default::default(),
         }
     }
 
@@ -238,7 +207,7 @@ impl Field {
             nullable,
             dict_id,
             dict_is_ordered,
-            metadata: HashMap::default(),
+            metadata: Default::default(),
         }
     }
 
@@ -368,25 +337,25 @@ impl Field {
 
     /// Sets the `Field`'s optional custom metadata.
     #[inline]
-    pub fn set_metadata(&mut self, metadata: HashMap<String, String>) {
-        self.metadata = metadata;
+    pub fn set_metadata(&mut self, metadata: impl Into<Metadata>) {
+        self.metadata = metadata.into();
     }
 
     /// Sets the metadata of this `Field` to be `metadata` and returns self
-    pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
+    pub fn with_metadata(mut self, metadata: impl Into<Metadata>) -> Self {
         self.set_metadata(metadata);
         self
     }
 
     /// Returns the immutable reference to the `Field`'s optional custom 
metadata.
     #[inline]
-    pub const fn metadata(&self) -> &HashMap<String, String> {
+    pub const fn metadata(&self) -> &Metadata {
         &self.metadata
     }
 
     /// Returns a mutable reference to the `Field`'s optional custom metadata.
     #[inline]
-    pub fn metadata_mut(&mut self) -> &mut HashMap<String, String> {
+    pub fn metadata_mut(&mut self) -> &mut Metadata {
         &mut self.metadata
     }
 
@@ -464,11 +433,8 @@ impl Field {
     /// let field = Field::new("", DataType::Null, false);
     /// assert_eq!(field.extension_type_name(), None);
     ///
-    /// let field = Field::new("", DataType::Null, false).with_metadata(
-    ///    [(EXTENSION_TYPE_NAME_KEY.to_owned(), "example".to_owned())]
-    ///        .into_iter()
-    ///        .collect(),
-    /// );
+    /// let field = Field::new("", DataType::Null, false)
+    ///     .with_metadata([(EXTENSION_TYPE_NAME_KEY, "example")]);
     /// assert_eq!(field.extension_type_name(), Some("example"));
     /// ```
     pub fn extension_type_name(&self) -> Option<&str> {
@@ -491,11 +457,8 @@ impl Field {
     /// let field = Field::new("", DataType::Null, false);
     /// assert_eq!(field.extension_type_metadata(), None);
     ///
-    /// let field = Field::new("", DataType::Null, false).with_metadata(
-    ///    [(EXTENSION_TYPE_METADATA_KEY.to_owned(), "example".to_owned())]
-    ///        .into_iter()
-    ///        .collect(),
-    /// );
+    /// let field = Field::new("", DataType::Null, false)
+    ///     .with_metadata([(EXTENSION_TYPE_METADATA_KEY, "example")]);
     /// assert_eq!(field.extension_type_metadata(), Some("example"));
     /// ```
     pub fn extension_type_metadata(&self) -> Option<&str> {
@@ -963,7 +926,7 @@ impl Field {
         std::mem::size_of_val(self) - std::mem::size_of_val(&self.data_type)
             + self.data_type.size()
             + self.name.capacity()
-            + (std::mem::size_of::<(String, String)>() * 
self.metadata.capacity())
+            + (std::mem::size_of::<(String, String)>() * self.metadata.len())
             + self
                 .metadata
                 .iter()
@@ -1009,6 +972,7 @@ impl std::fmt::Display for Field {
 #[cfg(test)]
 mod test {
     use super::*;
+    use std::collections::HashMap;
     use std::collections::hash_map::DefaultHasher;
 
     #[derive(Debug, Clone, Copy)]
@@ -1048,40 +1012,21 @@ mod test {
         let no_extension = Field::new("f", DataType::Null, false);
         assert!(!no_extension.has_valid_extension_type::<TestExtensionType>());
 
-        let matching_name = Field::new("f", DataType::Null, 
false).with_metadata(
-            [(
-                EXTENSION_TYPE_NAME_KEY.to_owned(),
-                TestExtensionType::NAME.to_owned(),
-            )]
-            .into_iter()
-            .collect(),
-        );
+        let matching_name = Field::new("f", DataType::Null, false)
+            .with_metadata([(EXTENSION_TYPE_NAME_KEY, 
TestExtensionType::NAME)]);
         assert!(matching_name.has_valid_extension_type::<TestExtensionType>());
 
         let matching_name_with_invalid_metadata = Field::new("f", 
DataType::Null, false)
-            .with_metadata(
-                [
-                    (
-                        EXTENSION_TYPE_NAME_KEY.to_owned(),
-                        TestExtensionType::NAME.to_owned(),
-                    ),
-                    (EXTENSION_TYPE_METADATA_KEY.to_owned(), 
"invalid".to_owned()),
-                ]
-                .into_iter()
-                .collect(),
-            );
+            .with_metadata([
+                (EXTENSION_TYPE_NAME_KEY, TestExtensionType::NAME),
+                (EXTENSION_TYPE_METADATA_KEY, "invalid"),
+            ]);
         assert!(
             
!matching_name_with_invalid_metadata.has_valid_extension_type::<TestExtensionType>()
         );
 
-        let different_name = Field::new("f", DataType::Null, 
false).with_metadata(
-            [(
-                EXTENSION_TYPE_NAME_KEY.to_owned(),
-                "some.other_extension".to_owned(),
-            )]
-            .into_iter()
-            .collect(),
-        );
+        let different_name = Field::new("f", DataType::Null, false)
+            .with_metadata([(EXTENSION_TYPE_NAME_KEY, 
"some.other_extension")]);
         
assert!(!different_name.has_valid_extension_type::<TestExtensionType>());
     }
 
diff --git a/arrow-schema/src/lib.rs b/arrow-schema/src/lib.rs
index 1eeeb4d106..4f6771a1c9 100644
--- a/arrow-schema/src/lib.rs
+++ b/arrow-schema/src/lib.rs
@@ -37,6 +37,8 @@ mod field;
 pub use field::*;
 mod fields;
 pub use fields::*;
+mod metadata;
+pub use metadata::*;
 mod schema;
 pub use schema::*;
 use std::ops;
diff --git a/arrow-schema/src/metadata.rs b/arrow-schema/src/metadata.rs
new file mode 100644
index 0000000000..7b9ec707c7
--- /dev/null
+++ b/arrow-schema/src/metadata.rs
@@ -0,0 +1,473 @@
+// 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.
+
+use std::collections::{BTreeMap, HashMap, btree_map};
+use std::fmt;
+use std::ops::Index;
+use std::sync::Arc;
+
+/// A cheaply clonable map of key-value metadata, used by
+/// [`Field`](crate::Field) and [`Schema`](crate::Schema).
+///
+/// Cloning a `Metadata` is always cheap, as the underlying map is
+/// reference-counted. Mutating a shared `Metadata` (e.g. via
+/// [`Metadata::insert`]) will clone the underlying map if (and only if)
+/// it is shared (copy-on-write).
+///
+/// The entries are stored in a [`BTreeMap`], so iteration order is
+/// deterministic (sorted by key).
+///
+/// # Example
+/// ```
+/// # use arrow_schema::Metadata;
+/// let mut metadata = Metadata::new();
+/// metadata.insert("key", "value");
+///
+/// let clone = metadata.clone(); // cheap
+/// assert_eq!(clone.get("key"), Some(&"value".to_string()));
+///
+/// // Mutating one does not affect the other:
+/// metadata.insert("key2", "value2");
+/// assert_eq!(metadata.len(), 2);
+/// assert_eq!(clone.len(), 1);
+/// ```
+#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct Metadata(
+    // Invariant: the inner map is never empty (`None` encodes the empty map).
+    // This ensures the derived implementations of `PartialEq`, `Ord`, `Hash`, 
…
+    // treat an empty `Metadata` consistently, and the empty case never 
allocates.
+    // We use `BTreeMap` for deterministic iteration order.
+    Option<Arc<BTreeMap<String, String>>>,
+);
+
+impl Metadata {
+    /// Creates an empty [`Metadata`].
+    ///
+    /// This does not allocate.
+    pub const fn new() -> Self {
+        Self(None)
+    }
+
+    /// Returns the number of entries.
+    pub fn len(&self) -> usize {
+        self.0.as_ref().map_or(0, |map| map.len())
+    }
+
+    /// Returns `true` if there are no entries.
+    pub fn is_empty(&self) -> bool {
+        self.0.is_none()
+    }
+
+    /// Returns a reference to the value corresponding to the key.
+    pub fn get(&self, key: &str) -> Option<&String> {
+        self.0.as_ref()?.get(key)
+    }
+
+    /// Returns `true` if the map contains a value for the specified key.
+    pub fn contains_key(&self, key: &str) -> bool {
+        self.0.as_ref().is_some_and(|map| map.contains_key(key))
+    }
+
+    /// Inserts a key-value pair, returning the old value of the key, if any.
+    ///
+    /// If the map already contained this key, the value is replaced.
+    ///
+    /// Clones the underlying map if (and only if) it is shared.
+    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) 
-> Option<String> {
+        Arc::make_mut(self.0.get_or_insert_default()).insert(key.into(), 
value.into())
+    }
+
+    /// Inserts a key-value pair and returns `self`, for builder-style 
chaining.
+    ///
+    /// If the map already contained this key, the value is replaced.
+    ///
+    /// # Example
+    /// ```
+    /// # use arrow_schema::Metadata;
+    /// let metadata = Metadata::new().with("a", "1").with("b", "2");
+    /// assert_eq!(metadata.len(), 2);
+    /// ```
+    #[must_use]
+    pub fn with(mut self, key: impl Into<String>, value: impl Into<String>) -> 
Self {
+        self.insert(key, value);
+        self
+    }
+
+    /// Removes a key from the map, returning the value at the key
+    /// if the key was previously in the map.
+    ///
+    /// Clones the underlying map if (and only if) it is shared and contains 
the key.
+    pub fn remove(&mut self, key: &str) -> Option<String> {
+        let map = self.0.as_mut()?;
+        if !map.contains_key(key) {
+            return None;
+        }
+        let removed = Arc::make_mut(map).remove(key);
+        if map.is_empty() {
+            self.0 = None;
+        }
+        removed
+    }
+
+    /// Removes all entries.
+    pub fn clear(&mut self) {
+        self.0 = None;
+    }
+
+    /// Returns an iterator over the entries, sorted by key.
+    pub fn iter(&self) -> MetadataIter<'_> {
+        self.0
+            .as_deref()
+            .map(|map| map.iter())
+            .into_iter()
+            .flatten()
+    }
+
+    /// Returns an iterator over the keys, in sorted order.
+    pub fn keys(&self) -> impl Iterator<Item = &String> {
+        self.iter().map(|(key, _)| key)
+    }
+
+    /// Returns an iterator over the values, sorted by key.
+    pub fn values(&self) -> impl Iterator<Item = &String> {
+        self.iter().map(|(_, value)| value)
+    }
+}
+
+/// Iterator over the entries of a [`Metadata`], sorted by key.
+pub type MetadataIter<'a> =
+    std::iter::Flatten<std::option::IntoIter<btree_map::Iter<'a, String, 
String>>>;
+
+impl fmt::Debug for Metadata {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_map().entries(self.iter()).finish()
+    }
+}
+
+impl Index<&str> for Metadata {
+    type Output = String;
+
+    fn index(&self, key: &str) -> &String {
+        self.get(key)
+            .unwrap_or_else(|| panic!("no entry found for key {key:?}"))
+    }
+}
+
+impl From<BTreeMap<String, String>> for Metadata {
+    fn from(map: BTreeMap<String, String>) -> Self {
+        if map.is_empty() {
+            Self(None)
+        } else {
+            Self(Some(Arc::new(map)))
+        }
+    }
+}
+
+impl From<HashMap<String, String>> for Metadata {
+    fn from(map: HashMap<String, String>) -> Self {
+        map.into_iter().collect::<BTreeMap<_, _>>().into()
+    }
+}
+
+impl From<Arc<BTreeMap<String, String>>> for Metadata {
+    fn from(map: Arc<BTreeMap<String, String>>) -> Self {
+        if map.is_empty() {
+            Self(None)
+        } else {
+            Self(Some(map))
+        }
+    }
+}
+
+impl<K: Into<String>, V: Into<String>, const N: usize> From<[(K, V); N]> for 
Metadata {
+    fn from(entries: [(K, V); N]) -> Self {
+        entries.into_iter().collect()
+    }
+}
+
+impl From<Metadata> for BTreeMap<String, String> {
+    fn from(metadata: Metadata) -> Self {
+        match metadata.0 {
+            None => BTreeMap::new(),
+            Some(map) => Arc::try_unwrap(map).unwrap_or_else(|map| 
(*map).clone()),
+        }
+    }
+}
+
+impl From<Metadata> for HashMap<String, String> {
+    fn from(metadata: Metadata) -> Self {
+        metadata.into_iter().collect()
+    }
+}
+
+impl From<&Metadata> for HashMap<String, String> {
+    fn from(metadata: &Metadata) -> Self {
+        metadata
+            .iter()
+            .map(|(k, v)| (k.clone(), v.clone()))
+            .collect()
+    }
+}
+
+impl<K: Into<String>, V: Into<String>> FromIterator<(K, V)> for Metadata {
+    fn from_iter<T: IntoIterator<Item = (K, V)>>(iter: T) -> Self {
+        iter.into_iter()
+            .map(|(k, v)| (k.into(), v.into()))
+            .collect::<BTreeMap<_, _>>()
+            .into()
+    }
+}
+
+impl<K: Into<String>, V: Into<String>> Extend<(K, V)> for Metadata {
+    fn extend<T: IntoIterator<Item = (K, V)>>(&mut self, iter: T) {
+        let mut iter = iter
+            .into_iter()
+            .map(|(k, v)| (k.into(), v.into()))
+            .peekable();
+        if iter.peek().is_none() {
+            return; // Avoid cloning a shared map for an empty iterator
+        }
+        Arc::make_mut(self.0.get_or_insert_default()).extend(iter);
+    }
+}
+
+impl<'a> IntoIterator for &'a Metadata {
+    type Item = (&'a String, &'a String);
+    type IntoIter = MetadataIter<'a>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.iter()
+    }
+}
+
+impl IntoIterator for Metadata {
+    type Item = (String, String);
+    type IntoIter = btree_map::IntoIter<String, String>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        BTreeMap::from(self).into_iter()
+    }
+}
+
+impl PartialEq<HashMap<String, String>> for Metadata {
+    fn eq(&self, other: &HashMap<String, String>) -> bool {
+        self.len() == other.len()
+            && self
+                .iter()
+                .all(|(k, v)| other.get(k).is_some_and(|other_v| v == other_v))
+    }
+}
+
+impl PartialEq<BTreeMap<String, String>> for Metadata {
+    fn eq(&self, other: &BTreeMap<String, String>) -> bool {
+        self.iter().eq(other.iter())
+    }
+}
+
+#[cfg(feature = "serde")]
+mod serde_impl {
+    use super::Metadata;
+    use std::collections::BTreeMap;
+
+    impl serde_core::Serialize for Metadata {
+        fn serialize<S: serde_core::Serializer>(&self, serializer: S) -> 
Result<S::Ok, S::Error> {
+            use serde_core::ser::SerializeMap as _;
+            let mut map = serializer.serialize_map(Some(self.len()))?;
+            for (key, value) in self {
+                map.serialize_entry(key, value)?;
+            }
+            map.end()
+        }
+    }
+
+    impl<'de> serde_core::Deserialize<'de> for Metadata {
+        fn deserialize<D: serde_core::Deserializer<'de>>(
+            deserializer: D,
+        ) -> Result<Self, D::Error> {
+            Ok(BTreeMap::<String, String>::deserialize(deserializer)?.into())
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_empty() {
+        let metadata = Metadata::new();
+        assert!(metadata.is_empty());
+        assert_eq!(metadata.len(), 0);
+        assert_eq!(metadata.get("key"), None);
+        assert!(!metadata.contains_key("key"));
+        assert_eq!(metadata.iter().count(), 0);
+        assert_eq!(metadata, Metadata::default());
+
+        // Empty maps don't allocate:
+        assert!(Metadata::from(HashMap::new()).0.is_none());
+        assert!(Metadata::from(BTreeMap::new()).0.is_none());
+        assert!(
+            std::iter::empty::<(String, String)>()
+                .collect::<Metadata>()
+                .0
+                .is_none()
+        );
+    }
+
+    #[test]
+    fn test_insert_get_remove() {
+        let mut metadata = Metadata::new();
+        assert_eq!(metadata.insert("a", "1"), None);
+        assert_eq!(metadata.insert("a", "2"), Some("1".to_string()));
+        assert_eq!(metadata.insert("b", "3"), None);
+
+        assert_eq!(metadata.len(), 2);
+        assert_eq!(metadata.get("a"), Some(&"2".to_string()));
+        assert_eq!(metadata["b"], "3");
+        assert!(metadata.contains_key("a"));
+        assert!(!metadata.contains_key("c"));
+
+        assert_eq!(metadata.remove("c"), None);
+        assert_eq!(metadata.remove("a"), Some("2".to_string()));
+        assert_eq!(metadata.remove("b"), Some("3".to_string()));
+
+        // Invariant: removing the last entry restores the unallocated state
+        assert!(metadata.is_empty());
+        assert!(metadata.0.is_none());
+    }
+
+    #[test]
+    fn test_clear() {
+        let mut metadata = Metadata::from([("a", "1")]);
+        metadata.clear();
+        assert!(metadata.is_empty());
+        assert!(metadata.0.is_none());
+    }
+
+    #[test]
+    #[should_panic(expected = "no entry found for key \"missing\"")]
+    fn test_index_panics() {
+        let metadata = Metadata::from([("a", "1")]);
+        let _ = &metadata["missing"];
+    }
+
+    #[test]
+    fn test_copy_on_write() {
+        let mut metadata = Metadata::from([("a", "1")]);
+        let clone = metadata.clone();
+
+        // Cloning is shallow:
+        let arc = metadata.0.as_ref().expect("non-empty");
+        assert!(Arc::ptr_eq(arc, clone.0.as_ref().expect("non-empty")));
+
+        // Mutation clones the shared map, leaving the clone untouched:
+        metadata.insert("b", "2");
+        assert_eq!(metadata.len(), 2);
+        assert_eq!(clone.len(), 1);
+
+        // Mutating an unshared map does not clone it:
+        let arc = metadata.0.as_ref().expect("non-empty").clone();
+        metadata.insert("c", "3");
+        assert!(
+            Arc::ptr_eq(&arc, metadata.0.as_ref().expect("non-empty"))
+                || Arc::strong_count(&arc) == 1
+        );
+
+        // Removing a missing key from a shared map does not clone it:
+        let clone = metadata.clone();
+        let arc = metadata.0.as_ref().expect("non-empty");
+        assert!(Arc::ptr_eq(arc, clone.0.as_ref().expect("non-empty")));
+        let mut metadata2 = metadata.clone();
+        assert_eq!(metadata2.remove("missing"), None);
+        assert!(Arc::ptr_eq(
+            metadata.0.as_ref().expect("non-empty"),
+            metadata2.0.as_ref().expect("non-empty")
+        ));
+    }
+
+    #[test]
+    fn test_deterministic_iteration_order() {
+        let metadata: Metadata = [("b", "2"), ("a", "1"), ("c", 
"3")].into_iter().collect();
+        let keys: Vec<&String> = metadata.keys().collect();
+        assert_eq!(keys, ["a", "b", "c"]);
+        let values: Vec<&String> = metadata.values().collect();
+        assert_eq!(values, ["1", "2", "3"]);
+    }
+
+    #[test]
+    fn test_eq_with_std_maps() {
+        let metadata = Metadata::from([("a", "1"), ("b", "2")]);
+
+        let hash_map: HashMap<String, String> = metadata.clone().into();
+        assert_eq!(metadata, hash_map);
+
+        let btree_map: BTreeMap<String, String> = metadata.clone().into();
+        assert_eq!(metadata, btree_map);
+
+        let mut different = hash_map.clone();
+        different.insert("c".to_string(), "3".to_string());
+        assert_ne!(metadata, different);
+
+        let mut different = hash_map;
+        different.insert("a".to_string(), "other".to_string());
+        assert_ne!(metadata, different);
+    }
+
+    #[test]
+    fn test_extend() {
+        let mut metadata = Metadata::new();
+        let clone = metadata.clone();
+        metadata.extend(std::iter::empty::<(String, String)>());
+        assert!(metadata.0.is_none()); // No allocation for an empty extend
+
+        metadata.extend([("a", "1"), ("b", "2")]);
+        assert_eq!(metadata.len(), 2);
+        assert!(clone.is_empty());
+    }
+
+    #[test]
+    fn test_debug() {
+        let metadata = Metadata::from([("b", "2"), ("a", "1")]);
+        assert_eq!(format!("{metadata:?}"), r#"{"a": "1", "b": "2"}"#);
+        assert_eq!(format!("{:?}", Metadata::new()), "{}");
+    }
+
+    #[test]
+    #[cfg(feature = "serde")]
+    fn test_serde_round_trip() {
+        for metadata in [Metadata::new(), Metadata::from([("a", "1"), ("b", 
"2")])] {
+            let serialized = 
postcard::to_stdvec(&metadata).expect("serialize");
+            let deserialized: Metadata = 
postcard::from_bytes(&serialized).expect("deserialize");
+            assert_eq!(metadata, deserialized);
+        }
+    }
+
+    #[test]
+    #[cfg(feature = "serde")]
+    fn test_serde_matches_std_map_format() {
+        // `Metadata` must serialize exactly like the maps it replaced
+        let metadata = Metadata::from([("a", "1"), ("b", "2")]);
+        let as_btree_map: BTreeMap<String, String> = metadata.clone().into();
+
+        let serialized = postcard::to_stdvec(&metadata).expect("serialize");
+        let map_serialized = 
postcard::to_stdvec(&as_btree_map).expect("serialize");
+        assert_eq!(serialized, map_serialized);
+
+        let deserialized: Metadata = 
postcard::from_bytes(&map_serialized).expect("deserialize");
+        assert_eq!(metadata, deserialized);
+    }
+}
diff --git a/arrow-schema/src/schema.rs b/arrow-schema/src/schema.rs
index 0c7db39dfb..aa9ef877b1 100644
--- a/arrow-schema/src/schema.rs
+++ b/arrow-schema/src/schema.rs
@@ -15,20 +15,19 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use std::collections::HashMap;
 use std::fmt;
 use std::hash::Hash;
 use std::sync::Arc;
 
 use crate::error::ArrowError;
 use crate::field::Field;
-use crate::{DataType, FieldRef, Fields};
+use crate::{DataType, FieldRef, Fields, Metadata};
 
 /// A builder to facilitate building a [`Schema`] from iteratively from 
[`FieldRef`]
 #[derive(Debug, Default)]
 pub struct SchemaBuilder {
     fields: Vec<FieldRef>,
-    metadata: HashMap<String, String>,
+    metadata: Metadata,
 }
 
 impl SchemaBuilder {
@@ -78,12 +77,12 @@ impl SchemaBuilder {
     }
 
     /// Returns an immutable reference to the Map of custom metadata key-value 
pairs.
-    pub fn metadata(&mut self) -> &HashMap<String, String> {
+    pub fn metadata(&mut self) -> &Metadata {
         &self.metadata
     }
 
     /// Returns a mutable reference to the Map of custom metadata key-value 
pairs.
-    pub fn metadata_mut(&mut self) -> &mut HashMap<String, String> {
+    pub fn metadata_mut(&mut self) -> &mut Metadata {
         &mut self.metadata
     }
 
@@ -182,13 +181,13 @@ pub type SchemaRef = Arc<Schema>;
 ///
 /// Note that this information is only part of the meta-data and not part of 
the physical
 /// memory layout.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
 pub struct Schema {
     /// A sequence of fields that describe the schema.
     pub fields: Fields,
     /// A map of key-value pairs containing additional metadata.
-    pub metadata: HashMap<String, String>,
+    pub metadata: Metadata,
 }
 
 impl Schema {
@@ -196,7 +195,7 @@ impl Schema {
     pub fn empty() -> Self {
         Self {
             fields: Default::default(),
-            metadata: HashMap::new(),
+            metadata: Default::default(),
         }
     }
 
@@ -212,7 +211,7 @@ impl Schema {
     /// let schema = Schema::new(vec![field_a, field_b]);
     /// ```
     pub fn new(fields: impl Into<Fields>) -> Self {
-        Self::new_with_metadata(fields, HashMap::new())
+        Self::new_with_metadata(fields, Metadata::new())
     }
 
     /// Creates a new [`Schema`] from a sequence of [`Field`] values
@@ -222,27 +221,22 @@ impl Schema {
     ///
     /// ```
     /// # use arrow_schema::*;
-    /// # use std::collections::HashMap;
-    ///
     /// let field_a = Field::new("a", DataType::Int64, false);
     /// let field_b = Field::new("b", DataType::Boolean, false);
     ///
-    /// let mut metadata: HashMap<String, String> = HashMap::new();
-    /// metadata.insert("row_count".to_string(), "100".to_string());
-    ///
-    /// let schema = Schema::new_with_metadata(vec![field_a, field_b], 
metadata);
+    /// let schema = Schema::new_with_metadata(vec![field_a, field_b], 
[("row_count", "100")]);
     /// ```
     #[inline]
-    pub fn new_with_metadata(fields: impl Into<Fields>, metadata: 
HashMap<String, String>) -> Self {
+    pub fn new_with_metadata(fields: impl Into<Fields>, metadata: impl 
Into<Metadata>) -> Self {
         Self {
             fields: fields.into(),
-            metadata,
+            metadata: metadata.into(),
         }
     }
 
     /// Sets the metadata of this `Schema` to be `metadata` and returns self
-    pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
-        self.metadata = metadata;
+    pub fn with_metadata(mut self, metadata: impl Into<Metadata>) -> Self {
+        self.metadata = metadata.into();
         self
     }
 
@@ -293,7 +287,7 @@ impl Schema {
     /// );
     /// ```
     pub fn try_merge(schemas: impl IntoIterator<Item = Self>) -> Result<Self, 
ArrowError> {
-        let mut out_meta = HashMap::new();
+        let mut out_meta = Metadata::new();
         let mut out_fields = SchemaBuilder::new();
         for schema in schemas {
             let Schema { metadata, fields } = schema;
@@ -407,7 +401,7 @@ impl Schema {
 
     /// Returns an immutable reference to the Map of custom metadata key-value 
pairs.
     #[inline]
-    pub const fn metadata(&self) -> &HashMap<String, String> {
+    pub const fn metadata(&self) -> &Metadata {
         &self.metadata
     }
 
@@ -530,22 +524,6 @@ impl fmt::Display for Schema {
     }
 }
 
-// need to implement `Hash` manually because `HashMap` implement Eq but no 
`Hash`
-#[allow(clippy::derived_hash_with_manual_eq)]
-impl Hash for Schema {
-    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-        self.fields.hash(state);
-
-        // ensure deterministic key order
-        let mut keys: Vec<&String> = self.metadata.keys().collect();
-        keys.sort();
-        for k in keys {
-            k.hash(state);
-            self.metadata.get(k).expect("key valid").hash(state);
-        }
-    }
-}
-
 impl AsRef<Schema> for Schema {
     fn as_ref(&self) -> &Schema {
         self
@@ -556,6 +534,7 @@ impl AsRef<Schema> for Schema {
 mod tests {
     use crate::datatype::DataType;
     use crate::{TimeUnit, UnionMode};
+    use std::collections::HashMap;
 
     use super::*;
 
@@ -594,8 +573,7 @@ mod tests {
         assert_eq!(schema, de_schema);
 
         // ser/de with non-empty metadata
-        let schema =
-            schema.with_metadata([("key".to_owned(), 
"val".to_owned())].into_iter().collect());
+        let schema = schema.with_metadata([("key", "val")]);
         let json = serde_json::to_string(&schema).unwrap();
         let de_schema = serde_json::from_str(&json).unwrap();
 
@@ -705,12 +683,7 @@ mod tests {
         assert_ne!(schema2, schema4);
         assert_ne!(schema3, schema4);
 
-        let f = Field::new("c1", DataType::Utf8, false).with_metadata(
-            [("foo".to_string(), "bar".to_string())]
-                .iter()
-                .cloned()
-                .collect(),
-        );
+        let f = Field::new("c1", DataType::Utf8, false).with_metadata([("foo", 
"bar")]);
         let schema5 = Schema::new(vec![
             f,
             Field::new("c2", DataType::Float64, true),
@@ -1275,49 +1248,24 @@ mod tests {
         assert_eq!(f1.metadata(), f2.metadata());
 
         // 3. Some + Some
-        let mut f1 = Field::new("first_name", DataType::Utf8, 
false).with_metadata(
-            [("foo".to_string(), "bar".to_string())]
-                .iter()
-                .cloned()
-                .collect(),
-        );
-        let f2 = Field::new("first_name", DataType::Utf8, false).with_metadata(
-            [("foo2".to_string(), "bar2".to_string())]
-                .iter()
-                .cloned()
-                .collect(),
-        );
+        let mut f1 =
+            Field::new("first_name", DataType::Utf8, 
false).with_metadata([("foo", "bar")]);
+        let f2 = Field::new("first_name", DataType::Utf8, 
false).with_metadata([("foo2", "bar2")]);
 
         assert!(f1.try_merge(&f2).is_ok());
         assert!(!f1.metadata().is_empty());
         assert_eq!(
-            f1.metadata().clone(),
-            [
-                ("foo".to_string(), "bar".to_string()),
-                ("foo2".to_string(), "bar2".to_string())
-            ]
-            .iter()
-            .cloned()
-            .collect()
+            f1.metadata(),
+            &Metadata::from([("foo", "bar"), ("foo2", "bar2")])
         );
 
         // 4. Some + None.
-        let mut f1 = Field::new("first_name", DataType::Utf8, 
false).with_metadata(
-            [("foo".to_string(), "bar".to_string())]
-                .iter()
-                .cloned()
-                .collect(),
-        );
+        let mut f1 =
+            Field::new("first_name", DataType::Utf8, 
false).with_metadata([("foo", "bar")]);
         let f2 = Field::new("first_name", DataType::Utf8, false);
         assert!(f1.try_merge(&f2).is_ok());
         assert!(!f1.metadata().is_empty());
-        assert_eq!(
-            f1.metadata().clone(),
-            [("foo".to_string(), "bar".to_string())]
-                .iter()
-                .cloned()
-                .collect()
-        );
+        assert_eq!(f1.metadata(), &Metadata::from([("foo", "bar")]));
 
         // 5. None + None.
         let mut f1 = Field::new("first_name", DataType::Utf8, false);
@@ -1491,12 +1439,12 @@ mod tests {
 
     #[test]
     fn test_schema_builder_metadata() {
-        let mut metadata = HashMap::with_capacity(1);
+        let mut metadata: HashMap<String, String> = HashMap::with_capacity(1);
         metadata.insert("key".to_string(), "value".to_string());
 
         let fields = vec![Field::new("test", DataType::Int8, true)];
         let mut builder: SchemaBuilder = 
Schema::new(fields).with_metadata(metadata).into();
-        builder.metadata_mut().insert("k".into(), "v".into());
+        builder.metadata_mut().insert("k", "v");
         let out = builder.finish();
         assert_eq!(out.metadata.len(), 2);
         assert_eq!(out.metadata["k"], "v");
diff --git a/arrow/src/datatypes/mod.rs b/arrow/src/datatypes/mod.rs
index 4286128a76..451fdf0251 100644
--- a/arrow/src/datatypes/mod.rs
+++ b/arrow/src/datatypes/mod.rs
@@ -27,6 +27,6 @@ pub use arrow_array::{ArrowNativeTypeOp, ArrowNumericType, 
ArrowPrimitiveType};
 pub use arrow_buffer::{ArrowNativeType, ToByteSlice, i256};
 pub use arrow_data::decimal::*;
 pub use arrow_schema::{
-    DataType, Field, FieldRef, Fields, IntervalUnit, Schema, SchemaBuilder, 
SchemaRef, TimeUnit,
-    UnionFields, UnionMode,
+    DataType, Field, FieldRef, Fields, IntervalUnit, Metadata, Schema, 
SchemaBuilder, SchemaRef,
+    TimeUnit, UnionFields, UnionMode,
 };
diff --git a/parquet/src/arrow/arrow_reader/mod.rs 
b/parquet/src/arrow/arrow_reader/mod.rs
index 2517e892fc..b41fdd111c 100644
--- a/parquet/src/arrow/arrow_reader/mod.rs
+++ b/parquet/src/arrow/arrow_reader/mod.rs
@@ -4082,9 +4082,7 @@ pub(crate) mod tests {
 
         let schema_without_metadata = 
Arc::new(Schema::new(vec![field.clone()]));
 
-        let metadata = [("key".to_string(), "value".to_string())]
-            .into_iter()
-            .collect();
+        let metadata = arrow_schema::Metadata::from([("key".to_string(), 
"value".to_string())]);
 
         let schema_with_metadata = 
Arc::new(Schema::new(vec![field.with_metadata(metadata)]));
 
diff --git a/parquet/src/arrow/arrow_writer/mod.rs 
b/parquet/src/arrow/arrow_writer/mod.rs
index 2a96170adc..58577e1bd3 100644
--- a/parquet/src/arrow/arrow_writer/mod.rs
+++ b/parquet/src/arrow/arrow_writer/mod.rs
@@ -4882,11 +4882,7 @@ mod tests {
     #[test]
     fn test_arrow_writer_metadata() {
         let batch_schema = Schema::new(vec![Field::new("int32", 
DataType::Int32, false)]);
-        let file_schema = batch_schema.clone().with_metadata(
-            vec![("foo".to_string(), "bar".to_string())]
-                .into_iter()
-                .collect(),
-        );
+        let file_schema = batch_schema.clone().with_metadata([("foo", "bar")]);
 
         let batch = RecordBatch::try_new(
             Arc::new(batch_schema),
diff --git a/parquet/src/arrow/schema/complex.rs 
b/parquet/src/arrow/schema/complex.rs
index 161b263028..99dc3d4dc7 100644
--- a/parquet/src/arrow/schema/complex.rs
+++ b/parquet/src/arrow/schema/complex.rs
@@ -493,10 +493,9 @@ impl Visitor {
                         .with_nullable(false),
                 );
                 let value_field = Arc::new(convert_field(map_value, &value, 
arrow_value, true)?);
-                let field_metadata = match arrow_map {
-                    Some(field) => field.metadata().clone(),
-                    _ => HashMap::default(),
-                };
+                let field_metadata = arrow_map
+                    .map(|field| field.metadata().clone())
+                    .unwrap_or_default();
 
                 let map_field = Field::new_struct(
                     map_key_value.name(),
diff --git a/parquet/src/arrow/schema/virtual_type.rs 
b/parquet/src/arrow/schema/virtual_type.rs
index f66352ae19..dc9e191e8f 100644
--- a/parquet/src/arrow/schema/virtual_type.rs
+++ b/parquet/src/arrow/schema/virtual_type.rs
@@ -153,11 +153,8 @@ mod tests {
     #[test]
     #[should_panic(expected = "Extension type name missing")]
     fn row_number_missing_name() {
-        let field = Field::new("", DataType::Int64, false).with_metadata(
-            [(EXTENSION_TYPE_METADATA_KEY.to_owned(), "".to_owned())]
-                .into_iter()
-                .collect(),
-        );
+        let field = Field::new("", DataType::Int64, false)
+            .with_metadata([(EXTENSION_TYPE_METADATA_KEY, "")]);
         field.extension_type::<RowNumber>();
     }
 
@@ -170,34 +167,18 @@ mod tests {
     #[test]
     #[should_panic(expected = "Virtual column extension type expects an empty 
string as metadata")]
     fn row_number_missing_metadata() {
-        let field = Field::new("", DataType::Int64, false).with_metadata(
-            [(
-                EXTENSION_TYPE_NAME_KEY.to_owned(),
-                RowNumber::NAME.to_owned(),
-            )]
-            .into_iter()
-            .collect(),
-        );
+        let field = Field::new("", DataType::Int64, false)
+            .with_metadata([(EXTENSION_TYPE_NAME_KEY, RowNumber::NAME)]);
         field.extension_type::<RowNumber>();
     }
 
     #[test]
     #[should_panic(expected = "Virtual column extension type expects an empty 
string as metadata")]
     fn row_number_invalid_metadata() {
-        let field = Field::new("", DataType::Int64, false).with_metadata(
-            [
-                (
-                    EXTENSION_TYPE_NAME_KEY.to_owned(),
-                    RowNumber::NAME.to_owned(),
-                ),
-                (
-                    EXTENSION_TYPE_METADATA_KEY.to_owned(),
-                    "non-empty".to_owned(),
-                ),
-            ]
-            .into_iter()
-            .collect(),
-        );
+        let field = Field::new("", DataType::Int64, false).with_metadata([
+            (EXTENSION_TYPE_NAME_KEY, RowNumber::NAME),
+            (EXTENSION_TYPE_METADATA_KEY, "non-empty"),
+        ]);
         field.extension_type::<RowNumber>();
     }
 
@@ -213,11 +194,8 @@ mod tests {
     #[test]
     #[should_panic(expected = "Extension type name missing")]
     fn row_group_index_missing_name() {
-        let field = Field::new("", DataType::Int64, false).with_metadata(
-            [(EXTENSION_TYPE_METADATA_KEY.to_owned(), "".to_owned())]
-                .into_iter()
-                .collect(),
-        );
+        let field = Field::new("", DataType::Int64, false)
+            .with_metadata([(EXTENSION_TYPE_METADATA_KEY, "")]);
         field.extension_type::<RowGroupIndex>();
     }
 
@@ -230,34 +208,18 @@ mod tests {
     #[test]
     #[should_panic(expected = "Virtual column extension type expects an empty 
string as metadata")]
     fn row_group_index_missing_metadata() {
-        let field = Field::new("", DataType::Int64, false).with_metadata(
-            [(
-                EXTENSION_TYPE_NAME_KEY.to_owned(),
-                RowGroupIndex::NAME.to_owned(),
-            )]
-            .into_iter()
-            .collect(),
-        );
+        let field = Field::new("", DataType::Int64, false)
+            .with_metadata([(EXTENSION_TYPE_NAME_KEY, RowGroupIndex::NAME)]);
         field.extension_type::<RowGroupIndex>();
     }
 
     #[test]
     #[should_panic(expected = "Virtual column extension type expects an empty 
string as metadata")]
     fn row_group_index_invalid_metadata() {
-        let field = Field::new("", DataType::Int64, false).with_metadata(
-            [
-                (
-                    EXTENSION_TYPE_NAME_KEY.to_owned(),
-                    RowGroupIndex::NAME.to_owned(),
-                ),
-                (
-                    EXTENSION_TYPE_METADATA_KEY.to_owned(),
-                    "non-empty".to_owned(),
-                ),
-            ]
-            .into_iter()
-            .collect(),
-        );
+        let field = Field::new("", DataType::Int64, false).with_metadata([
+            (EXTENSION_TYPE_NAME_KEY, RowGroupIndex::NAME),
+            (EXTENSION_TYPE_METADATA_KEY, "non-empty"),
+        ]);
         field.extension_type::<RowGroupIndex>();
     }
 }

Reply via email to