PyArrow to JSON Schema Specification¶
This document specifies how a PyArrow pa.Schema determines:
- The Draft 2020-12 JSON Schema document produced by
generate_json_schema(andPyArrowToJSONSchema), including physical integer bounds, fixed-point decimal patterns, Base64 binary encodings, key/value map arrays, and losslessx-pyarrowextension metadata. - The serialized JSON row dictionaries produced by
serialize_table_for_jsonschemaandserialize_record_for_jsonschemafrom Arrowpa.Tables or Python row mappings, ready for Draft 2020-12 JSON Schema validation. - The errors raised for Arrow schemas or data types that have no faithful Draft 2020-12 JSON Schema representation.
Contents: 1. Conversion Rules · 2. Cases · 3. Errors · 4. Fixture Index
1. Conversion Rules¶
1.1 Top-Level Schema Resolution and Envelope¶
- R-SCHEMA-INPUT: The input to
generate_json_schemamust be a concretepyarrow.Schemainstance; passing a barepa.DataType,pa.Field,pa.Table, or Python mapping raises E1.
Cases: 01_integers_floats_and_booleans, checkm2_quality_report
- R-NON-EMPTY-ROOT: The root
pa.Schemamust contain at least one field; an emptypa.schema([])raises E2.
Cases: 01_integers_floats_and_booleans
- R-DIALECT: Standalone documents always emit
"$schema": "https://json-schema.org/draft/2020-12/schema". Whendestinationis supplied togenerate_json_schema, the converted schema (requiring a non-emptyschema_nameormetadata[b"title"]) is inserted underdestination["$defs"][<name>]with the inner"$schema"keyword stripped.
Cases: 01_integers_floats_and_booleans, 05_metadata_omission_and_overrides, checkm2_quality_report
- R-ENVELOPE-ID-TITLE: Top-level
"title"resolves from explicitschema_namewhen provided, otherwise from non-empty UTF-8schema.metadata[b"title"], and is omitted if neither is set. Top-level"$id"resolves from explicitschema_idwhen provided, otherwise from non-empty UTF-8schema.metadata[b"$id"], and is omitted if neither is set.
Cases: 04_nullability_and_metadata, 05_metadata_omission_and_overrides, checkm2_quality_report, ncbi_assembly_summary, uniref_cluster_entry
- R-ENVELOPE-CLOSED: Every root schema and nested
pa.structemits"type": "object","properties": {...}, and"additionalProperties": false. Top-level keywords appear in canonical order:$schema,$id,title,description,type,properties,additionalProperties,required,x-pyarrow.
Cases: 01_integers_floats_and_booleans, 04_nullability_and_metadata, 05_metadata_omission_and_overrides, checkm2_quality_report
1.2 Field Naming, Ordering, and Uniqueness¶
- R-FIELD-NAME: Every
pa.Field.namein a rootpa.Schemaor nestedpa.StructTypeis preserved verbatim as its property key in"properties"and (when non-nullable) in"required".
Cases: 01_integers_floats_and_booleans, 04_nullability_and_metadata, checkm2_quality_report
- R-FIELD-ORDER:
"properties"keys and"required"entries preserve the exact declaration order of the sourcepa.Schemaorpa.StructType. When every field of an object hasnullable=True,"required"is omitted rather than emitted as[].
Cases: 01_integers_floats_and_booleans, 04_nullability_and_metadata, checkm2_quality_report
- R-UNIQUE-FIELD-NAMES: Sibling fields within a
pa.Schemaorpa.StructTypemust have distinct names; duplicate sibling field names raise E3.
Cases: 01_integers_floats_and_booleans
1.3 Structural Container Shapes¶
- R-SHAPE-STRUCT: A nested
pa.struct([...])with at least one child field maps to a closed JSON Schema"object"("additionalProperties": false) carrying"x-pyarrow": {"arrow_type": "struct"}. An empty nestedpa.struct([])raises E2.
Cases: 06_structs_and_lists, 07_maps_and_dictionaries, checkm2_quality_report, ncbi_assembly_summary, uniref_cluster_entry
- R-SHAPE-LIST: Variable-length Arrow list types (
pa.list_,pa.large_list,pa.list_view,pa.large_list_view) map to{"type": "array", "items": <item schema>}with"x-pyarrow": {"arrow_type": "<variant>"}("list","large_list","list_view", or"large_list_view"). Multi-dimensional lists (list<list<T>>) nest"array"schemas recursively.
Cases: 06_structs_and_lists, checkm2_quality_report, ncbi_assembly_summary, uniref_cluster_entry
- R-SHAPE-FIXED-LIST: Fixed-length
pa.list_(item, list_size)(pa.FixedSizeListType) maps to{"type": "array", "items": <item schema>, "minItems": list_size, "maxItems": list_size}with"x-pyarrow": {"arrow_type": "fixed_size_list", "list_size": list_size}.
Cases: 06_structs_and_lists, uniref_cluster_entry
- R-SHAPE-MAP:
pa.map_(key_type, item_type, keys_sorted=...)maps to an"array"of closed key/value entry objects ("properties": {"key": <key schema>, "value": <value schema>},"required": ["key", "value"],"additionalProperties": false), carrying"description": "PyArrow map rendered as key/value pairs (JSON object keys must be strings)."and"x-pyarrow": {"arrow_type": "map", "keys_sorted": <bool>}, supporting both string and non-string Arrow key types.
Cases: 07_maps_and_dictionaries, checkm2_quality_report, uniref_cluster_entry
- R-SHAPE-DICTIONARY:
pa.dictionary(index_type, value_type, ordered=...)emits the JSON Schema core type and constraints ofvalue_typewhile recording"arrow_type": "dictionary","value_type","index_type", and"ordered"inside"x-pyarrow".
Cases: 07_maps_and_dictionaries, checkm2_quality_report, ncbi_assembly_summary
1.4 Field and Element Nullability¶
- R-NULL-REQUIRED: A field with
pa.Field.nullable == Falseis included in its enclosing object's"required"array and its property subschema is emitted directly without a null union wrapper.
Cases: 01_integers_floats_and_booleans, 02_strings_binary_and_decimals, 03_temporal_types, 04_nullability_and_metadata, 05_metadata_omission_and_overrides, 06_structs_and_lists, 07_maps_and_dictionaries, checkm2_quality_report, ncbi_assembly_summary, uniref_cluster_entry
- R-NULL-OPTIONAL: A field with
pa.Field.nullable == Trueis omitted from"required"and its value subschema (including its type-level"x-pyarrow"block) is wrapped in{"anyOf": [<non-null schema>, {"type": "null"}]}. Standalonepa.null()columns or element types have no value domain and raise E4.
Cases: 02_strings_binary_and_decimals, 03_temporal_types, 04_nullability_and_metadata, 05_metadata_omission_and_overrides, 06_structs_and_lists, 07_maps_and_dictionaries, checkm2_quality_report, ncbi_assembly_summary, uniref_cluster_entry
- R-NULL-LIST-ITEMS: A list type's inner
value_field.nullablecontrols element nullability independently of the outer list field:value_field.nullable == Falseemits<item schema>directly under"items", whereasvalue_field.nullable == Truewraps"items"in{"anyOf": [<item schema>, {"type": "null"}]}.
Cases: 06_structs_and_lists
- R-NULL-MAP-VALUES: Arrow map keys are always non-nullable (
"key"has no null union). WhenMapType.item_field.nullableisTrue, the entry object's"value"property is wrapped in{"anyOf": [<value schema>, {"type": "null"}]}while both"key"and"value"remain in the entry object's"required"array.
Cases: 07_maps_and_dictionaries
1.5 Scalar Type Mappings¶
- R-SCALAR-BOOL:
pa.bool_()maps to{"type": "boolean"}with"x-pyarrow": {"arrow_type": "bool"}.
Cases: 01_integers_floats_and_booleans, ncbi_assembly_summary
- R-SCALAR-SIGNED-INT: Signed integer types (
int8,int16,int32,int64) map to"type": "integer"with exact two's-complement bounds (minimum: -2^(w-1),maximum: 2^(w-1) - 1for bit widthw) and"x-pyarrow": {"arrow_type": "int8"|"int16"|"int32"|"int64"}.
Cases: 01_integers_floats_and_booleans, checkm2_quality_report, ncbi_assembly_summary
- R-SCALAR-UNSIGNED-INT: Unsigned integer types (
uint8,uint16,uint32,uint64) map to"type": "integer"withminimum: 0,maximum: 2^w - 1for bit widthw, and"x-pyarrow": {"arrow_type": "uint8"|"uint16"|"uint32"|"uint64"}.
Cases: 01_integers_floats_and_booleans, checkm2_quality_report, ncbi_assembly_summary, uniref_cluster_entry
- R-SCALAR-FLOAT: IEEE-754 floating-point types (
float16,float32,float64) map to{"type": "number"}with"x-pyarrow": {"arrow_type": "float16"|"float32"|"float64"}.
Cases: 01_integers_floats_and_booleans, checkm2_quality_report, uniref_cluster_entry
- R-SCALAR-DECIMAL:
pa.decimal128(p, s)andpa.decimal256(p, s)with0 <= s <= pmap to{"type": "string", "pattern": "<fixed-point regex>"}enforcing at mostp - sinteger digits andsfractional digits, plus"x-pyarrow": {"arrow_type": "decimal128"|"decimal256", "logical_type": "decimal", "precision": p, "scale": s}. Decimals withs < 0ors > praise E5.
Cases: 02_strings_binary_and_decimals, checkm2_quality_report, ncbi_assembly_summary
- R-SCALAR-STRING: UTF-8 string types (
string,large_string,string_view) map to{"type": "string"}with"x-pyarrow": {"arrow_type": "string"|"large_string"|"string_view"}.
Cases: 02_strings_binary_and_decimals, checkm2_quality_report, ncbi_assembly_summary, uniref_cluster_entry
- R-SCALAR-BINARY: Variable-length binary types (
binary,large_binary,binary_view) map to{"type": "string", "contentEncoding": "base64"}with"x-pyarrow": {"arrow_type": "binary"|"large_binary"|"binary_view"}. Fixed-widthpa.binary(n)(FixedSizeBinaryType) maps to{"type": "string", "contentEncoding": "base64"}with"x-pyarrow": {"arrow_type": "fixed_size_binary", "byte_width": n}.
Cases: 02_strings_binary_and_decimals, ncbi_assembly_summary, uniref_cluster_entry
- R-SCALAR-DATE-TIME:
date32anddate64map to{"type": "string", "format": "date"}(arrow_type:"date32"or"date64").time32("s"|"ms")andtime64("us"|"ns")map to{"type": "string", "format": "time"}with"x-pyarrow": {"arrow_type": "time32"|"time64", "unit": "<unit>"}. Relative durations and calendar intervals (pa.duration,pa.month_day_nano_interval) raise E6.
Cases: 03_temporal_types, ncbi_assembly_summary
- R-SCALAR-TIMESTAMP: Timezone-aware
pa.timestamp(unit, tz=...)(non-emptytz) maps to{"type": "string", "format": "date-time"}with"x-pyarrow": {"arrow_type": "timestamp", "logical_type": "timestamp-with-tz", "unit": "<unit>", "tz": "<tz>"}. Timezone-naivepa.timestamp(unit)(tz=None) omits"format", sets"description": "ISO 8601 timestamp with no timezone attached."on the scalar node, and records"x-pyarrow": {"arrow_type": "timestamp", "logical_type": "timestamp-without-tz", "unit": "<unit>"}. Arrow unions (sparse_union,dense_union) raise E7;run_end_encodedandBaseExtensionTypetypes (pa.uuid(), customExtensionType) raise E8.
Cases: 03_temporal_types, ncbi_assembly_summary, uniref_cluster_entry
PyArrow DataType |
Serialized JSON value | JSON Schema (core) | x-pyarrow |
|---|---|---|---|
pa.bool_() |
bool |
{"type": "boolean"} |
{"arrow_type": "bool"} |
pa.int8() |
int |
{"type": "integer", "minimum": -128, "maximum": 127} |
{"arrow_type": "int8"} |
pa.int16() |
int |
{"type": "integer", "minimum": -32768, "maximum": 32767} |
{"arrow_type": "int16"} |
pa.int32() |
int |
{"type": "integer", "minimum": -2147483648, "maximum": 2147483647} |
{"arrow_type": "int32"} |
pa.int64() |
int |
{"type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807} |
{"arrow_type": "int64"} |
pa.uint8() |
int |
{"type": "integer", "minimum": 0, "maximum": 255} |
{"arrow_type": "uint8"} |
pa.uint16() |
int |
{"type": "integer", "minimum": 0, "maximum": 65535} |
{"arrow_type": "uint16"} |
pa.uint32() |
int |
{"type": "integer", "minimum": 0, "maximum": 4294967295} |
{"arrow_type": "uint32"} |
pa.uint64() |
int |
{"type": "integer", "minimum": 0, "maximum": 18446744073709551615} |
{"arrow_type": "uint64"} |
pa.float16() |
float |
{"type": "number"} |
{"arrow_type": "float16"} |
pa.float32() |
float |
{"type": "number"} |
{"arrow_type": "float32"} |
pa.float64() |
float |
{"type": "number"} |
{"arrow_type": "float64"} |
pa.decimal128(10, 2) |
str (fixed-point decimal) |
{"type": "string", "pattern": "^-?\\d{1,8}(\\.\\d{1,2})?$"} |
{"arrow_type": "decimal128", "logical_type": "decimal", "precision": 10, "scale": 2} |
pa.decimal128(2, 2) |
str (fixed-point decimal) |
{"type": "string", "pattern": "^-?0(\\.\\d{1,2})?$"} |
{"arrow_type": "decimal128", "logical_type": "decimal", "precision": 2, "scale": 2} |
pa.decimal128(10, 0) |
str (fixed-point integer decimal) |
{"type": "string", "pattern": "^-?\\d{1,10}$"} |
{"arrow_type": "decimal128", "logical_type": "decimal", "precision": 10, "scale": 0} |
pa.decimal256(50, 10) |
str (fixed-point decimal) |
{"type": "string", "pattern": "^-?\\d{1,40}(\\.\\d{1,10})?$"} |
{"arrow_type": "decimal256", "logical_type": "decimal", "precision": 50, "scale": 10} |
pa.string() |
str |
{"type": "string"} |
{"arrow_type": "string"} |
pa.large_string() |
str |
{"type": "string"} |
{"arrow_type": "large_string"} |
pa.string_view() |
str |
{"type": "string"} |
{"arrow_type": "string_view"} |
pa.binary() |
str (Base64) |
{"type": "string", "contentEncoding": "base64"} |
{"arrow_type": "binary"} |
pa.large_binary() |
str (Base64) |
{"type": "string", "contentEncoding": "base64"} |
{"arrow_type": "large_binary"} |
pa.binary_view() |
str (Base64) |
{"type": "string", "contentEncoding": "base64"} |
{"arrow_type": "binary_view"} |
pa.binary(16) |
str (Base64) |
{"type": "string", "contentEncoding": "base64"} |
{"arrow_type": "fixed_size_binary", "byte_width": 16} |
pa.date32() |
str (ISO-8601 date) |
{"type": "string", "format": "date"} |
{"arrow_type": "date32"} |
pa.date64() |
str (ISO-8601 date) |
{"type": "string", "format": "date"} |
{"arrow_type": "date64"} |
pa.time32("s") |
str (ISO-8601 time) |
{"type": "string", "format": "time"} |
{"arrow_type": "time32", "unit": "s"} |
pa.time32("ms") |
str (ISO-8601 time) |
{"type": "string", "format": "time"} |
{"arrow_type": "time32", "unit": "ms"} |
pa.time64("us") |
str (ISO-8601 time) |
{"type": "string", "format": "time"} |
{"arrow_type": "time64", "unit": "us"} |
pa.time64("ns") |
str (ISO-8601 time) |
{"type": "string", "format": "time"} |
{"arrow_type": "time64", "unit": "ns"} |
pa.timestamp("s", tz="UTC") |
str (ISO-8601 date-time) |
{"type": "string", "format": "date-time"} |
{"arrow_type": "timestamp", "logical_type": "timestamp-with-tz", "unit": "s", "tz": "UTC"} |
pa.timestamp("us", tz="UTC") |
str (ISO-8601 date-time) |
{"type": "string", "format": "date-time"} |
{"arrow_type": "timestamp", "logical_type": "timestamp-with-tz", "unit": "us", "tz": "UTC"} |
pa.timestamp("ns", tz="America/Los_Angeles") |
str (ISO-8601 date-time) |
{"type": "string", "format": "date-time"} |
{"arrow_type": "timestamp", "logical_type": "timestamp-with-tz", "unit": "ns", "tz": "America/Los_Angeles"} |
pa.timestamp("us") |
str (naive ISO-8601 timestamp) |
{"type": "string", "description": "ISO 8601 timestamp with no timezone attached."} |
{"arrow_type": "timestamp", "logical_type": "timestamp-without-tz", "unit": "us"} |
pa.timestamp("ns") |
str (naive ISO-8601 timestamp) |
{"type": "string", "description": "ISO 8601 timestamp with no timezone attached."} |
{"arrow_type": "timestamp", "logical_type": "timestamp-without-tz", "unit": "ns"} |
pa.dictionary(pa.int16(), pa.string()) |
str |
{"type": "string"} |
{"arrow_type": "dictionary", "value_type": "string", "index_type": "int16", "ordered": false} |
pa.dictionary(pa.int8(), pa.int64(), ordered=True) |
int |
{"type": "integer", "minimum": -9223372036854775808, "maximum": 9223372036854775807} |
{"arrow_type": "dictionary", "value_type": "int64", "index_type": "int8", "ordered": true} |
1.6 Descriptions and x-pyarrow Extensions¶
- R-META-DESCRIPTION: Non-empty UTF-8
metadata[b"comment"]on apa.Schema,pa.Field, listvalue_field, or mapkey_field/item_fieldis emitted as JSON Schema"description"and excluded fromx-pyarrow.metadata. On nullable fields (nullable=True), field-level"description"and field-level"x-pyarrow"metadata are placed on the outer property schema outside"anyOf".
Cases: 04_nullability_and_metadata, 05_metadata_omission_and_overrides, 06_structs_and_lists, 07_maps_and_dictionaries, checkm2_quality_report, ncbi_assembly_summary, uniref_cluster_entry
- R-META-X-PYARROW-TYPE: When
include_pyarrow_metadata=True(the default), every converted scalar, dictionary, struct, list, and map schema node carries a validated"x-pyarrow"object recordingarrow_typeand applicable parametric fields (logical_type,precision,scale,unit,tz,byte_width,list_size,keys_sorted,index_type,value_type,ordered).
Cases: 01_integers_floats_and_booleans, 02_strings_binary_and_decimals, 03_temporal_types, 04_nullability_and_metadata, 06_structs_and_lists, 07_maps_and_dictionaries, checkm2_quality_report, ncbi_assembly_summary, uniref_cluster_entry
- R-META-X-PYARROW-CUSTOM: Custom UTF-8 key/value entries in
pa.Schema.metadata(excludingcomment,title,$id),pa.Field.metadata(excludingcomment), and list/map child field metadata are preserved under"x-pyarrow": {"metadata": {...}}wheninclude_pyarrow_metadata=True.
Cases: 04_nullability_and_metadata, 06_structs_and_lists, uniref_cluster_entry
- R-META-OMIT-FLAG: Setting
include_pyarrow_metadata=Falseomits all"x-pyarrow"blocks at the document, field, and nested node levels, yielding a pure Draft 2020-12 JSON Schema while retaining"$id","title", and"description".
Cases: 05_metadata_omission_and_overrides
1.7 Arrow Table and Record Serialization¶
- R-SER-ROW-ORDER:
serialize_table_for_jsonschemaandserialize_record_for_jsonschemaemit one JSON object per row with keys inpa.Schemadeclaration order; null Arrow scalars serialize asnull(None), structs serialize as nested dicts, lists serialize as JSON arrays, and dictionary scalars unwrap to their decoded value.
Cases: 01_integers_floats_and_booleans, 02_strings_binary_and_decimals, 03_temporal_types, 04_nullability_and_metadata, 05_metadata_omission_and_overrides, 06_structs_and_lists, 07_maps_and_dictionaries, checkm2_quality_report, ncbi_assembly_summary, uniref_cluster_entry
- R-SER-DECIMAL-STR:
decimal128anddecimal256scalars serialize via plain fixed-point formatting (format(value, "f"), never scientific notation) matching the emittedpatternregex.
Cases: 02_strings_binary_and_decimals, 07_maps_and_dictionaries, checkm2_quality_report, ncbi_assembly_summary
- R-SER-BASE64:
binary,large_binary,binary_view, andfixed_size_binary(n)scalars serialize to ASCII Base64 strings matching"contentEncoding": "base64".
Cases: 02_strings_binary_and_decimals, ncbi_assembly_summary, uniref_cluster_entry
- R-SER-TEMPORAL-ISO:
date32anddate64serialize asYYYY-MM-DD;time32andtime64serialize asHH:MM:SS[.fffffffff](preserving sub-microsecond nanosecond digits when non-zero); timezone-awaretimestampserializes with its explicit ISO-8601 UTC/IANA offset; timezone-naivetimestampserializes without a timezone suffix.
Cases: 03_temporal_types, ncbi_assembly_summary, uniref_cluster_entry
- R-SER-MAP-ENTRIES:
pa.map_scalars serialize as[{"key": <serialized key>, "value": <serialized value>}, ...]preserving entry order and typed non-string keys.
Cases: 07_maps_and_dictionaries, checkm2_quality_report, uniref_cluster_entry
- R-SER-FINITE-FLOAT: Finite
float16,float32, andfloat64scalars serialize as JSONfloatnumbers; non-finiteNaNor±Infinityvalues raiseValueError.
Cases: 01_integers_floats_and_booleans
2. Cases¶
Booleans, Signed and Unsigned Integers, and IEEE-754 Floats (spec/01_integers_floats_and_booleans)¶
Rules: R-SCHEMA-INPUT, R-NON-EMPTY-ROOT, R-DIALECT, R-ENVELOPE-CLOSED, R-FIELD-NAME, R-FIELD-ORDER, R-UNIQUE-FIELD-NAMES, R-NULL-REQUIRED, R-SCALAR-BOOL, R-SCALAR-SIGNED-INT, R-SCALAR-UNSIGNED-INT, R-SCALAR-FLOAT, R-META-X-PYARROW-TYPE, R-SER-ROW-ORDER, R-SER-FINITE-FLOAT
Exercises exact two's-complement minimum/maximum bounds for int8..int64, unsigned 0..2^w - 1 bounds
for uint8..uint64 (including full 64-bit 18446744073709551615), bool_, and float16/float32/float64
at boundary values.
Schema (schema.py)¶
"""PyArrow schema for 01_integers_floats_and_booleans."""
import pyarrow as pa
SCHEMA = pa.schema(
[
pa.field("flag", pa.bool_(), nullable=False),
pa.field("i8", pa.int8(), nullable=False),
pa.field("i16", pa.int16(), nullable=False),
pa.field("i32", pa.int32(), nullable=False),
pa.field("i64", pa.int64(), nullable=False),
pa.field("u8", pa.uint8(), nullable=False),
pa.field("u16", pa.uint16(), nullable=False),
pa.field("u32", pa.uint32(), nullable=False),
pa.field("u64", pa.uint64(), nullable=False),
pa.field("f16", pa.float16(), nullable=False),
pa.field("f32", pa.float32(), nullable=False),
pa.field("f64", pa.float64(), nullable=False),
]
)
Input (table.py)¶
"""Sample PyArrow table for 01_integers_floats_and_booleans."""
import numpy as np
import pyarrow as pa
from schema import SCHEMA
TABLE = pa.Table.from_pylist(
[
{
"flag": True,
"i8": -128,
"i16": -32768,
"i32": -2147483648,
"i64": -9223372036854775808,
"u8": 0,
"u16": 0,
"u32": 0,
"u64": 0,
"f16": np.float16(-1.5),
"f32": -0.25,
"f64": -123456.7890125,
},
{
"flag": False,
"i8": 127,
"i16": 32767,
"i32": 2147483647,
"i64": 9223372036854775807,
"u8": 255,
"u16": 65535,
"u32": 4294967295,
"u64": 18446744073709551615,
"f16": np.float16(3.5),
"f32": 2.5,
"f64": 0.0,
},
],
schema=SCHEMA,
)
Serialized records: run boundary_values¶
[
{
"flag": true,
"i8": -128,
"i16": -32768,
"i32": -2147483648,
"i64": -9223372036854775808,
"u8": 0,
"u16": 0,
"u32": 0,
"u64": 0,
"f16": -1.5,
"f32": -0.25,
"f64": -123456.7890125
},
{
"flag": false,
"i8": 127,
"i16": 32767,
"i32": 2147483647,
"i64": 9223372036854775807,
"u8": 255,
"u16": 65535,
"u32": 4294967295,
"u64": 18446744073709551615,
"f16": 3.5,
"f32": 2.5,
"f64": 0.0
}
]
JSON Schema (expected_schema.json)¶
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"flag": {
"type": "boolean",
"x-pyarrow": {
"arrow_type": "bool"
}
},
"i8": {
"type": "integer",
"minimum": -128,
"maximum": 127,
"x-pyarrow": {
"arrow_type": "int8"
}
},
"i16": {
"type": "integer",
"minimum": -32768,
"maximum": 32767,
"x-pyarrow": {
"arrow_type": "int16"
}
},
"i32": {
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647,
"x-pyarrow": {
"arrow_type": "int32"
}
},
"i64": {
"type": "integer",
"minimum": -9223372036854775808,
"maximum": 9223372036854775807,
"x-pyarrow": {
"arrow_type": "int64"
}
},
"u8": {
"type": "integer",
"minimum": 0,
"maximum": 255,
"x-pyarrow": {
"arrow_type": "uint8"
}
},
"u16": {
"type": "integer",
"minimum": 0,
"maximum": 65535,
"x-pyarrow": {
"arrow_type": "uint16"
}
},
"u32": {
"type": "integer",
"minimum": 0,
"maximum": 4294967295,
"x-pyarrow": {
"arrow_type": "uint32"
}
},
"u64": {
"type": "integer",
"minimum": 0,
"maximum": 18446744073709551615,
"x-pyarrow": {
"arrow_type": "uint64"
}
},
"f16": {
"type": "number",
"x-pyarrow": {
"arrow_type": "float16"
}
},
"f32": {
"type": "number",
"x-pyarrow": {
"arrow_type": "float32"
}
},
"f64": {
"type": "number",
"x-pyarrow": {
"arrow_type": "float64"
}
}
},
"additionalProperties": false,
"required": [
"flag",
"i8",
"i16",
"i32",
"i64",
"u8",
"u16",
"u32",
"u64",
"f16",
"f32",
"f64"
]
}
Strings, Binary Encodings, and Fixed-Point Decimals (spec/02_strings_binary_and_decimals)¶
Rules: R-SCALAR-STRING, R-SCALAR-BINARY, R-SCALAR-DECIMAL, R-NULL-REQUIRED, R-NULL-OPTIONAL, R-META-X-PYARROW-TYPE, R-SER-ROW-ORDER, R-SER-DECIMAL-STR, R-SER-BASE64
Covers UTF-8 string variants (string, large_string, string_view), Base64-encoded binary variants
(binary, large_binary, binary_view, fixed_size_binary), and regex-constrained fixed-point decimals
(decimal128 with scale < precision, scale == precision, scale == 0, and decimal256 including tiny
values formatted without scientific notation).
Schema (schema.py)¶
"""PyArrow schema for 02_strings_binary_and_decimals."""
import pyarrow as pa
SCHEMA = pa.schema(
[
pa.field("utf8_str", pa.string(), nullable=False),
pa.field("large_str", pa.large_string(), nullable=False),
pa.field("view_str", pa.string_view(), nullable=False),
pa.field("raw_bin", pa.binary(), nullable=False),
pa.field("large_bin", pa.large_binary(), nullable=False),
pa.field("view_bin", pa.binary_view(), nullable=False),
pa.field("fixed_bin_4", pa.binary(4), nullable=False),
pa.field("dec_standard", pa.decimal128(10, 2), nullable=False),
pa.field("dec_fraction_only", pa.decimal128(4, 4), nullable=False),
pa.field("dec_integer_only", pa.decimal128(8, 0), nullable=False),
pa.field("dec256_high_prec", pa.decimal256(50, 10), nullable=True),
]
)
Input (table.py)¶
"""Sample PyArrow table for 02_strings_binary_and_decimals."""
from decimal import Decimal
import pyarrow as pa
from schema import SCHEMA
TABLE = pa.Table.from_pylist(
[
{
"utf8_str": "contig_0001",
"large_str": "ATGCNATGCNATGCN",
"view_str": "short-view",
"raw_bin": b"\x00\x01\x02\xff",
"large_bin": b"kbase-large-payload",
"view_bin": b"binary-view-bytes",
"fixed_bin_4": b"\xde\xad\xbe\xef",
"dec_standard": Decimal("-12345678.95"),
"dec_fraction_only": Decimal("0.0625"),
"dec_integer_only": Decimal(98765432),
"dec256_high_prec": Decimal("0.0000000001"),
},
{
"utf8_str": "",
"large_str": "second-large-string",
"view_str": "inlined-or-out-of-line-string-view-value",
"raw_bin": b"",
"large_bin": b"\x89PNG",
"view_bin": b"1234567890abcdef",
"fixed_bin_4": b"\x00\x00\x00\x01",
"dec_standard": Decimal("0.00"),
"dec_fraction_only": Decimal("-0.9999"),
"dec_integer_only": Decimal(-42),
"dec256_high_prec": None,
},
],
schema=SCHEMA,
)
Serialized records: run default¶
[
{
"utf8_str": "contig_0001",
"large_str": "ATGCNATGCNATGCN",
"view_str": "short-view",
"raw_bin": "AAEC/w==",
"large_bin": "a2Jhc2UtbGFyZ2UtcGF5bG9hZA==",
"view_bin": "YmluYXJ5LXZpZXctYnl0ZXM=",
"fixed_bin_4": "3q2+7w==",
"dec_standard": "-12345678.95",
"dec_fraction_only": "0.0625",
"dec_integer_only": "98765432",
"dec256_high_prec": "0.0000000001"
},
{
"utf8_str": "",
"large_str": "second-large-string",
"view_str": "inlined-or-out-of-line-string-view-value",
"raw_bin": "",
"large_bin": "iVBORw==",
"view_bin": "MTIzNDU2Nzg5MGFiY2RlZg==",
"fixed_bin_4": "AAAAAQ==",
"dec_standard": "0.00",
"dec_fraction_only": "-0.9999",
"dec_integer_only": "-42",
"dec256_high_prec": null
}
]
JSON Schema (expected_schema.json)¶
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"utf8_str": {
"type": "string",
"x-pyarrow": {
"arrow_type": "string"
}
},
"large_str": {
"type": "string",
"x-pyarrow": {
"arrow_type": "large_string"
}
},
"view_str": {
"type": "string",
"x-pyarrow": {
"arrow_type": "string_view"
}
},
"raw_bin": {
"type": "string",
"contentEncoding": "base64",
"x-pyarrow": {
"arrow_type": "binary"
}
},
"large_bin": {
"type": "string",
"contentEncoding": "base64",
"x-pyarrow": {
"arrow_type": "large_binary"
}
},
"view_bin": {
"type": "string",
"contentEncoding": "base64",
"x-pyarrow": {
"arrow_type": "binary_view"
}
},
"fixed_bin_4": {
"type": "string",
"contentEncoding": "base64",
"x-pyarrow": {
"arrow_type": "fixed_size_binary",
"byte_width": 4
}
},
"dec_standard": {
"type": "string",
"pattern": "^-?\\d{1,8}(\\.\\d{1,2})?$",
"x-pyarrow": {
"arrow_type": "decimal128",
"logical_type": "decimal",
"precision": 10,
"scale": 2
}
},
"dec_fraction_only": {
"type": "string",
"pattern": "^-?0(\\.\\d{1,4})?$",
"x-pyarrow": {
"arrow_type": "decimal128",
"logical_type": "decimal",
"precision": 4,
"scale": 4
}
},
"dec_integer_only": {
"type": "string",
"pattern": "^-?\\d{1,8}$",
"x-pyarrow": {
"arrow_type": "decimal128",
"logical_type": "decimal",
"precision": 8,
"scale": 0
}
},
"dec256_high_prec": {
"anyOf": [
{
"type": "string",
"pattern": "^-?\\d{1,40}(\\.\\d{1,10})?$",
"x-pyarrow": {
"arrow_type": "decimal256",
"logical_type": "decimal",
"precision": 50,
"scale": 10
}
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"required": [
"utf8_str",
"large_str",
"view_str",
"raw_bin",
"large_bin",
"view_bin",
"fixed_bin_4",
"dec_standard",
"dec_fraction_only",
"dec_integer_only"
]
}
Dates, Times, and Timezone-Aware vs Timezone-Naive Timestamps (spec/03_temporal_types)¶
Rules: R-SCALAR-DATE-TIME, R-SCALAR-TIMESTAMP, R-NULL-REQUIRED, R-NULL-OPTIONAL, R-META-X-PYARROW-TYPE, R-SER-ROW-ORDER, R-SER-TEMPORAL-ISO
Covers date32, date64, time32("s"|"ms"), time64("us"|"ns") (including sub-microsecond nanoseconds),
timezone-aware timestamp across units (s, ms, us, ns) and IANA/UTC zones ("format": "date-time"),
and timezone-naive timestamp (logical_type: "timestamp-without-tz" without "format": "date-time").
Schema (schema.py)¶
"""PyArrow schema for 03_temporal_types."""
import pyarrow as pa
SCHEMA = pa.schema(
[
pa.field("d32", pa.date32(), nullable=False),
pa.field("d64", pa.date64(), nullable=False),
pa.field("t32_s", pa.time32("s"), nullable=False),
pa.field("t32_ms", pa.time32("ms"), nullable=False),
pa.field("t64_us", pa.time64("us"), nullable=False),
pa.field("t64_ns", pa.time64("ns"), nullable=False),
pa.field("ts_s_utc", pa.timestamp("s", tz="UTC"), nullable=False),
pa.field("ts_ms_utc", pa.timestamp("ms", tz="UTC"), nullable=False),
pa.field("ts_us_la", pa.timestamp("us", tz="America/Los_Angeles"), nullable=False),
pa.field("ts_ns_utc", pa.timestamp("ns", tz="UTC"), nullable=False),
pa.field("ts_naive_us", pa.timestamp("us"), nullable=False),
pa.field("ts_naive_ns_opt", pa.timestamp("ns"), nullable=True),
]
)
Input (table.py)¶
"""Sample PyArrow table for 03_temporal_types."""
import datetime as dt
import pyarrow as pa
from schema import SCHEMA
TABLE = pa.table(
{
"d32": pa.array([dt.date(2026, 4, 10), dt.date(1970, 1, 1)], type=pa.date32()),
"d64": pa.array([dt.date(2026, 4, 11), dt.date(2000, 2, 29)], type=pa.date64()),
"t32_s": pa.array([dt.time(8, 30, 0), dt.time(23, 59, 59)], type=pa.time32("s")),
"t32_ms": pa.array([dt.time(8, 30, 0, 250000), dt.time(0, 0, 0)], type=pa.time32("ms")),
"t64_us": pa.array([dt.time(14, 15, 30, 123456), dt.time(12, 0, 0)], type=pa.time64("us")),
"t64_ns": pa.array([52_200_123_456_789, 0], type=pa.time64("ns")),
"ts_s_utc": pa.array(
[
dt.datetime(2026, 4, 10, 14, 30, 0, tzinfo=dt.UTC),
dt.datetime(2025, 1, 1, 0, 0, 0, tzinfo=dt.UTC),
],
type=pa.timestamp("s", tz="UTC"),
),
"ts_ms_utc": pa.array(
[
dt.datetime(2026, 4, 10, 14, 30, 0, 500000, tzinfo=dt.UTC),
dt.datetime(2025, 1, 1, 0, 0, 0, tzinfo=dt.UTC),
],
type=pa.timestamp("ms", tz="UTC"),
),
"ts_us_la": pa.array(
[
dt.datetime(2026, 4, 10, 14, 30, 0, 125000, tzinfo=dt.UTC),
dt.datetime(2025, 1, 15, 18, 0, 0, tzinfo=dt.UTC),
],
type=pa.timestamp("us", tz="America/Los_Angeles"),
),
"ts_ns_utc": pa.array(
[
dt.datetime(2026, 4, 10, 14, 30, 0, 750000, tzinfo=dt.UTC),
dt.datetime(2025, 6, 1, 6, 0, 0, tzinfo=dt.UTC),
],
type=pa.timestamp("ns", tz="UTC"),
),
"ts_naive_us": pa.array(
[
dt.datetime(2026, 4, 10, 9, 45, 0, tzinfo=dt.UTC).replace(tzinfo=None),
dt.datetime(2025, 12, 31, 23, 59, 59, tzinfo=dt.UTC).replace(tzinfo=None),
],
type=pa.timestamp("us"),
),
"ts_naive_ns_opt": pa.array(
[
dt.datetime(2026, 4, 10, 9, 45, 0, 500000, tzinfo=dt.UTC).replace(tzinfo=None),
None,
],
type=pa.timestamp("ns"),
),
},
schema=SCHEMA,
)
Serialized records: run default¶
[
{
"d32": "2026-04-10",
"d64": "2026-04-11",
"t32_s": "08:30:00",
"t32_ms": "08:30:00.250000",
"t64_us": "14:15:30.123456",
"t64_ns": "14:30:00.123456789",
"ts_s_utc": "2026-04-10T14:30:00+00:00",
"ts_ms_utc": "2026-04-10T14:30:00.500000+00:00",
"ts_us_la": "2026-04-10T07:30:00.125000-07:00",
"ts_ns_utc": "2026-04-10T14:30:00.750000+00:00",
"ts_naive_us": "2026-04-10T09:45:00",
"ts_naive_ns_opt": "2026-04-10T09:45:00.500000"
},
{
"d32": "1970-01-01",
"d64": "2000-02-29",
"t32_s": "23:59:59",
"t32_ms": "00:00:00",
"t64_us": "12:00:00",
"t64_ns": "00:00:00",
"ts_s_utc": "2025-01-01T00:00:00+00:00",
"ts_ms_utc": "2025-01-01T00:00:00+00:00",
"ts_us_la": "2025-01-15T10:00:00-08:00",
"ts_ns_utc": "2025-06-01T06:00:00+00:00",
"ts_naive_us": "2025-12-31T23:59:59",
"ts_naive_ns_opt": null
}
]
JSON Schema (expected_schema.json)¶
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"d32": {
"type": "string",
"format": "date",
"x-pyarrow": {
"arrow_type": "date32"
}
},
"d64": {
"type": "string",
"format": "date",
"x-pyarrow": {
"arrow_type": "date64"
}
},
"t32_s": {
"type": "string",
"format": "time",
"x-pyarrow": {
"arrow_type": "time32",
"unit": "s"
}
},
"t32_ms": {
"type": "string",
"format": "time",
"x-pyarrow": {
"arrow_type": "time32",
"unit": "ms"
}
},
"t64_us": {
"type": "string",
"format": "time",
"x-pyarrow": {
"arrow_type": "time64",
"unit": "us"
}
},
"t64_ns": {
"type": "string",
"format": "time",
"x-pyarrow": {
"arrow_type": "time64",
"unit": "ns"
}
},
"ts_s_utc": {
"type": "string",
"format": "date-time",
"x-pyarrow": {
"arrow_type": "timestamp",
"logical_type": "timestamp-with-tz",
"unit": "s",
"tz": "UTC"
}
},
"ts_ms_utc": {
"type": "string",
"format": "date-time",
"x-pyarrow": {
"arrow_type": "timestamp",
"logical_type": "timestamp-with-tz",
"unit": "ms",
"tz": "UTC"
}
},
"ts_us_la": {
"type": "string",
"format": "date-time",
"x-pyarrow": {
"arrow_type": "timestamp",
"logical_type": "timestamp-with-tz",
"unit": "us",
"tz": "America/Los_Angeles"
}
},
"ts_ns_utc": {
"type": "string",
"format": "date-time",
"x-pyarrow": {
"arrow_type": "timestamp",
"logical_type": "timestamp-with-tz",
"unit": "ns",
"tz": "UTC"
}
},
"ts_naive_us": {
"type": "string",
"description": "ISO 8601 timestamp with no timezone attached.",
"x-pyarrow": {
"arrow_type": "timestamp",
"logical_type": "timestamp-without-tz",
"unit": "us"
}
},
"ts_naive_ns_opt": {
"anyOf": [
{
"type": "string",
"description": "ISO 8601 timestamp with no timezone attached.",
"x-pyarrow": {
"arrow_type": "timestamp",
"logical_type": "timestamp-without-tz",
"unit": "ns"
}
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"required": [
"d32",
"d64",
"t32_s",
"t32_ms",
"t64_us",
"t64_ns",
"ts_s_utc",
"ts_ms_utc",
"ts_us_la",
"ts_ns_utc",
"ts_naive_us"
]
}
Nullability, Envelope Metadata, Descriptions, and Custom Arrow Metadata (spec/04_nullability_and_metadata)¶
Rules: R-ENVELOPE-ID-TITLE, R-ENVELOPE-CLOSED, R-FIELD-NAME, R-FIELD-ORDER, R-NULL-REQUIRED, R-NULL-OPTIONAL, R-META-DESCRIPTION, R-META-X-PYARROW-TYPE, R-META-X-PYARROW-CUSTOM, R-SER-ROW-ORDER
Demonstrates resolution of "title", "$id", and "description" from pa.Schema.metadata, preservation of
custom schema and field metadata under x-pyarrow.metadata, placement of field "description" and
x-pyarrow.metadata outside "anyOf" on nullable fields, and omission of "required" on a nested struct
whose fields are all nullable.
Schema (schema.py)¶
"""PyArrow schema for 04_nullability_and_metadata."""
import pyarrow as pa
SCHEMA = pa.schema(
[
pa.field(
"sample_id",
pa.string(),
nullable=False,
metadata={
"comment": "Unique biosample identifier.",
"pydantic_type": "string",
"source_column": "biosample_acc",
},
),
pa.field(
"depth_m",
pa.float64(),
nullable=True,
metadata={
"comment": "Sampling depth in meters below surface.",
"unit": "m",
},
),
pa.field(
"quality_score",
pa.int32(),
nullable=True,
),
pa.field(
"optional_details",
pa.struct(
[
pa.field("collector", pa.string(), nullable=True, metadata={"comment": "Field collector name."}),
pa.field("ph", pa.float32(), nullable=True),
]
),
nullable=True,
metadata={"comment": "Optional environmental annotations where every child field is nullable."},
),
],
metadata={
"title": "EnvironmentalSample",
"$id": "https://kbase.us/schemas/environmental_sample.json",
"comment": "Environmental sample record with rich Arrow metadata.",
"dataset_version": "2026.04",
"curator": "cdm-ingest",
},
)
Input (table.py)¶
"""Sample PyArrow table for 04_nullability_and_metadata."""
import pyarrow as pa
from schema import SCHEMA
TABLE = pa.Table.from_pylist(
[
{
"sample_id": "SAMN00010001",
"depth_m": 125.5,
"quality_score": 98,
"optional_details": {"collector": "A. Smith", "ph": 7.25},
},
{
"sample_id": "SAMN00010002",
"depth_m": None,
"quality_score": None,
"optional_details": None,
},
],
schema=SCHEMA,
)
Serialized records: run default¶
[
{
"sample_id": "SAMN00010001",
"depth_m": 125.5,
"quality_score": 98,
"optional_details": {
"collector": "A. Smith",
"ph": 7.25
}
},
{
"sample_id": "SAMN00010002",
"depth_m": null,
"quality_score": null,
"optional_details": null
}
]
JSON Schema (expected_schema.json)¶
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://kbase.us/schemas/environmental_sample.json",
"title": "EnvironmentalSample",
"description": "Environmental sample record with rich Arrow metadata.",
"type": "object",
"properties": {
"sample_id": {
"type": "string",
"x-pyarrow": {
"arrow_type": "string",
"metadata": {
"pydantic_type": "string",
"source_column": "biosample_acc"
}
},
"description": "Unique biosample identifier."
},
"depth_m": {
"anyOf": [
{
"type": "number",
"x-pyarrow": {
"arrow_type": "float64"
}
},
{
"type": "null"
}
],
"description": "Sampling depth in meters below surface.",
"x-pyarrow": {
"metadata": {
"unit": "m"
}
}
},
"quality_score": {
"anyOf": [
{
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647,
"x-pyarrow": {
"arrow_type": "int32"
}
},
{
"type": "null"
}
]
},
"optional_details": {
"anyOf": [
{
"type": "object",
"properties": {
"collector": {
"anyOf": [
{
"type": "string",
"x-pyarrow": {
"arrow_type": "string"
}
},
{
"type": "null"
}
],
"description": "Field collector name."
},
"ph": {
"anyOf": [
{
"type": "number",
"x-pyarrow": {
"arrow_type": "float32"
}
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"x-pyarrow": {
"arrow_type": "struct"
}
},
{
"type": "null"
}
],
"description": "Optional environmental annotations where every child field is nullable."
}
},
"additionalProperties": false,
"required": [
"sample_id"
],
"x-pyarrow": {
"metadata": {
"dataset_version": "2026.04",
"curator": "cdm-ingest"
}
}
}
Explicit Envelope Overrides and Omitting x-pyarrow Metadata (spec/05_metadata_omission_and_overrides)¶
Rules: R-DIALECT, R-ENVELOPE-ID-TITLE, R-ENVELOPE-CLOSED, R-META-DESCRIPTION, R-META-OMIT-FLAG, R-NULL-REQUIRED, R-NULL-OPTIONAL, R-SER-ROW-ORDER
Verifies that explicit schema_name and schema_id parameters override metadata[b"title"] and
metadata[b"$id"], and that include_pyarrow_metadata=False strips all "x-pyarrow" blocks while keeping
standard Draft 2020-12 keywords ($schema, $id, title, description, minimum, maximum, pattern,
format, contentEncoding).
Schema (schema.py)¶
"""PyArrow schema for 05_metadata_omission_and_overrides."""
import pyarrow as pa
SCHEMA = pa.schema(
[
pa.field("id", pa.string(), nullable=False, metadata={"comment": "Record identifier.", "xsd_type": "xs:ID"}),
pa.field("count", pa.uint16(), nullable=False, metadata={"unit": "reads"}),
pa.field("gc_ratio", pa.decimal128(5, 4), nullable=True, metadata={"comment": "GC fraction."}),
pa.field("digest", pa.binary(8), nullable=False),
pa.field("collected_on", pa.date32(), nullable=True),
],
metadata={
"title": "OverriddenTitle",
"$id": "urn:overridden:sample",
"comment": "Pure Draft 2020-12 schema with x-pyarrow suppressed.",
"internal_tag": "drop-when-omitted",
},
)
Input (table.py)¶
"""Sample PyArrow table for 05_metadata_omission_and_overrides."""
import datetime as dt
from decimal import Decimal
import pyarrow as pa
from schema import SCHEMA
TABLE = pa.Table.from_pylist(
[
{
"id": "S-01",
"count": 1024,
"gc_ratio": Decimal("0.5125"),
"digest": b"01234567",
"collected_on": dt.date(2026, 3, 15),
},
{
"id": "S-02",
"count": 0,
"gc_ratio": None,
"digest": b"\x00\x01\x02\x03\x04\x05\x06\x07",
"collected_on": None,
},
],
schema=SCHEMA,
)
Serialized records: run without_pyarrow_metadata (schema_name=CleanSample, schema_id=https://kbase.us/schemas/clean_sample.json, include_pyarrow_metadata=False)¶
[
{
"id": "S-01",
"count": 1024,
"gc_ratio": "0.5125",
"digest": "MDEyMzQ1Njc=",
"collected_on": "2026-03-15"
},
{
"id": "S-02",
"count": 0,
"gc_ratio": null,
"digest": "AAECAwQFBgc=",
"collected_on": null
}
]
JSON Schema (expected_schema.json)¶
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://kbase.us/schemas/clean_sample.json",
"title": "CleanSample",
"description": "Pure Draft 2020-12 schema with x-pyarrow suppressed.",
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Record identifier."
},
"count": {
"type": "integer",
"minimum": 0,
"maximum": 65535
},
"gc_ratio": {
"anyOf": [
{
"type": "string",
"pattern": "^-?\\d{1,1}(\\.\\d{1,4})?$"
},
{
"type": "null"
}
],
"description": "GC fraction."
},
"digest": {
"type": "string",
"contentEncoding": "base64"
},
"collected_on": {
"anyOf": [
{
"type": "string",
"format": "date"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"required": [
"id",
"count",
"digest"
]
}
Nested Structs, Variable/View Lists, Fixed-Size Lists, and 2D Lists (spec/06_structs_and_lists)¶
Rules: R-SHAPE-STRUCT, R-SHAPE-LIST, R-SHAPE-FIXED-LIST, R-NULL-REQUIRED, R-NULL-OPTIONAL, R-NULL-LIST-ITEMS, R-META-DESCRIPTION, R-META-X-PYARROW-TYPE, R-META-X-PYARROW-CUSTOM, R-SER-ROW-ORDER
Exercises nested pa.struct, pa.list_, pa.large_list, pa.list_view, pa.large_list_view,
pa.list_(item, 3) (minItems: 3, maxItems: 3), 2D list<list<int32>>, nullable vs non-nullable list
elements, and element-level comment + custom metadata on inner pa.field items.
Schema (schema.py)¶
"""PyArrow schema for 06_structs_and_lists."""
import pyarrow as pa
SCHEMA = pa.schema(
[
pa.field("feature_id", pa.string(), nullable=False),
pa.field(
"location",
pa.struct(
[
pa.field("contig", pa.string(), nullable=False),
pa.field("start", pa.int64(), nullable=False),
pa.field("end", pa.int64(), nullable=False),
pa.field("strand", pa.string(), nullable=True),
]
),
nullable=False,
),
pa.field(
"go_terms",
pa.list_(
pa.field(
"item",
pa.string(),
nullable=False,
metadata={"comment": "Gene Ontology term accession.", "ontology": "GO"},
)
),
nullable=False,
),
pa.field(
"nullable_scores",
pa.large_list(pa.field("item", pa.int32(), nullable=True)),
nullable=True,
),
pa.field(
"aliases_view",
pa.list_view(pa.field("item", pa.string(), nullable=False)),
nullable=False,
),
pa.field(
"notes_large_view",
pa.large_list_view(pa.field("item", pa.string(), nullable=True)),
nullable=True,
),
pa.field(
"rgb_triplet",
pa.list_(pa.field("item", pa.uint8(), nullable=False), 3),
nullable=False,
),
pa.field(
"score_matrix",
pa.list_(
pa.field(
"row",
pa.list_(pa.field("cell", pa.int32(), nullable=False)),
nullable=False,
)
),
nullable=False,
),
]
)
Input (table.py)¶
"""Sample PyArrow table for 06_structs_and_lists."""
import pyarrow as pa
from schema import SCHEMA
TABLE = pa.Table.from_pylist(
[
{
"feature_id": "CDS_0001",
"location": {"contig": "chr1", "start": 100, "end": 950, "strand": "+"},
"go_terms": ["GO:0003677", "GO:0006355"],
"nullable_scores": [10, None, 42],
"aliases_view": ["dnaA", "b3702"],
"notes_large_view": ["verified", None],
"rgb_triplet": [255, 128, 0],
"score_matrix": [[1, 2], [3, 4, 5]],
},
{
"feature_id": "CDS_0002",
"location": {"contig": "chr2", "start": 2050, "end": 3100, "strand": None},
"go_terms": [],
"nullable_scores": None,
"aliases_view": [],
"notes_large_view": None,
"rgb_triplet": [0, 64, 128],
"score_matrix": [[]],
},
],
schema=SCHEMA,
)
Serialized records: run default¶
[
{
"feature_id": "CDS_0001",
"location": {
"contig": "chr1",
"start": 100,
"end": 950,
"strand": "+"
},
"go_terms": [
"GO:0003677",
"GO:0006355"
],
"nullable_scores": [
10,
null,
42
],
"aliases_view": [
"dnaA",
"b3702"
],
"notes_large_view": [
"verified",
null
],
"rgb_triplet": [
255,
128,
0
],
"score_matrix": [
[
1,
2
],
[
3,
4,
5
]
]
},
{
"feature_id": "CDS_0002",
"location": {
"contig": "chr2",
"start": 2050,
"end": 3100,
"strand": null
},
"go_terms": [],
"nullable_scores": null,
"aliases_view": [],
"notes_large_view": null,
"rgb_triplet": [
0,
64,
128
],
"score_matrix": [
[]
]
}
]
JSON Schema (expected_schema.json)¶
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"feature_id": {
"type": "string",
"x-pyarrow": {
"arrow_type": "string"
}
},
"location": {
"type": "object",
"properties": {
"contig": {
"type": "string",
"x-pyarrow": {
"arrow_type": "string"
}
},
"start": {
"type": "integer",
"minimum": -9223372036854775808,
"maximum": 9223372036854775807,
"x-pyarrow": {
"arrow_type": "int64"
}
},
"end": {
"type": "integer",
"minimum": -9223372036854775808,
"maximum": 9223372036854775807,
"x-pyarrow": {
"arrow_type": "int64"
}
},
"strand": {
"anyOf": [
{
"type": "string",
"x-pyarrow": {
"arrow_type": "string"
}
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"required": [
"contig",
"start",
"end"
],
"x-pyarrow": {
"arrow_type": "struct"
}
},
"go_terms": {
"type": "array",
"items": {
"type": "string",
"description": "Gene Ontology term accession.",
"x-pyarrow": {
"arrow_type": "string",
"metadata": {
"ontology": "GO"
}
}
},
"x-pyarrow": {
"arrow_type": "list"
}
},
"nullable_scores": {
"anyOf": [
{
"type": "array",
"items": {
"anyOf": [
{
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647,
"x-pyarrow": {
"arrow_type": "int32"
}
},
{
"type": "null"
}
]
},
"x-pyarrow": {
"arrow_type": "large_list"
}
},
{
"type": "null"
}
]
},
"aliases_view": {
"type": "array",
"items": {
"type": "string",
"x-pyarrow": {
"arrow_type": "string"
}
},
"x-pyarrow": {
"arrow_type": "list_view"
}
},
"notes_large_view": {
"anyOf": [
{
"type": "array",
"items": {
"anyOf": [
{
"type": "string",
"x-pyarrow": {
"arrow_type": "string"
}
},
{
"type": "null"
}
]
},
"x-pyarrow": {
"arrow_type": "large_list_view"
}
},
{
"type": "null"
}
]
},
"rgb_triplet": {
"type": "array",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 255,
"x-pyarrow": {
"arrow_type": "uint8"
}
},
"minItems": 3,
"maxItems": 3,
"x-pyarrow": {
"arrow_type": "fixed_size_list",
"list_size": 3
}
},
"score_matrix": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647,
"x-pyarrow": {
"arrow_type": "int32"
}
},
"x-pyarrow": {
"arrow_type": "list"
}
},
"x-pyarrow": {
"arrow_type": "list"
}
}
},
"additionalProperties": false,
"required": [
"feature_id",
"location",
"go_terms",
"aliases_view",
"rgb_triplet",
"score_matrix"
]
}
String-Keyed and Non-String-Keyed Maps and Dictionary-Encoded Columns (spec/07_maps_and_dictionaries)¶
Rules: R-SHAPE-MAP, R-SHAPE-DICTIONARY, R-SHAPE-STRUCT, R-NULL-REQUIRED, R-NULL-OPTIONAL, R-NULL-MAP-VALUES, R-META-DESCRIPTION, R-META-X-PYARROW-TYPE, R-SER-ROW-ORDER, R-SER-MAP-ENTRIES, R-SER-DECIMAL-STR
Exercises pa.map_ with string keys and nullable struct values, pa.map_ with sorted int32 keys and
non-nullable float64 values, and pa.dictionary columns wrapping string, int64 (ordered=True), and
decimal128(8, 2).
Schema (schema.py)¶
"""PyArrow schema for 07_maps_and_dictionaries."""
import pyarrow as pa
ANNOTATION_VALUE_STRUCT = pa.struct(
[
pa.field("db", pa.string(), nullable=False),
pa.field("evalue", pa.float64(), nullable=True),
]
)
SCHEMA = pa.schema(
[
pa.field("entry_id", pa.string(), nullable=False),
pa.field(
"biotype",
pa.dictionary(pa.int16(), pa.string(), ordered=False),
nullable=False,
metadata={"comment": "Controlled biotype term."},
),
pa.field(
"priority_tier",
pa.dictionary(pa.int8(), pa.int64(), ordered=True),
nullable=True,
),
pa.field(
"rate_bucket",
pa.dictionary(pa.int8(), pa.decimal128(8, 2), ordered=False),
nullable=False,
),
pa.field(
"xref_hits",
pa.map_(
pa.field("key", pa.string(), nullable=False, metadata={"comment": "External database key."}),
pa.field("value", ANNOTATION_VALUE_STRUCT, nullable=True),
),
nullable=False,
),
pa.field(
"coverage_by_bin",
pa.map_(
pa.int32(),
pa.field("value", pa.float64(), nullable=False),
keys_sorted=True,
),
nullable=True,
),
]
)
Input (table.py)¶
"""Sample PyArrow table for 07_maps_and_dictionaries."""
from decimal import Decimal
import pyarrow as pa
from schema import ANNOTATION_VALUE_STRUCT, SCHEMA
TABLE = pa.Table.from_arrays(
[
pa.array(["PROT-001", "PROT-002"], type=pa.string()),
pa.DictionaryArray.from_arrays(
pa.array([0, 1], type=pa.int16()),
pa.array(["protein_coding", "rRNA"], type=pa.string()),
),
pa.DictionaryArray.from_arrays(
pa.array([0, None], type=pa.int8()),
pa.array([1], type=pa.int64()),
ordered=True,
),
pa.DictionaryArray.from_arrays(
pa.array([0, 1], type=pa.int8()),
pa.array([Decimal("12.50"), Decimal("0.75")], type=pa.decimal128(8, 2)),
),
pa.array(
[
[("PF00001", {"db": "Pfam", "evalue": 1.25e-12}), ("IPR000276", None)],
[],
],
type=pa.map_(
pa.field("key", pa.string(), nullable=False, metadata={"comment": "External database key."}),
pa.field("value", ANNOTATION_VALUE_STRUCT, nullable=True),
),
),
pa.array(
[[(1, 45.5), (2, 88.25)], None],
type=pa.map_(pa.int32(), pa.field("value", pa.float64(), nullable=False), keys_sorted=True),
),
],
schema=SCHEMA,
)
Serialized records: run default¶
[
{
"entry_id": "PROT-001",
"biotype": "protein_coding",
"priority_tier": 1,
"rate_bucket": "12.50",
"xref_hits": [
{
"key": "PF00001",
"value": {
"db": "Pfam",
"evalue": 1.25e-12
}
},
{
"key": "IPR000276",
"value": null
}
],
"coverage_by_bin": [
{
"key": 1,
"value": 45.5
},
{
"key": 2,
"value": 88.25
}
]
},
{
"entry_id": "PROT-002",
"biotype": "rRNA",
"priority_tier": null,
"rate_bucket": "0.75",
"xref_hits": [],
"coverage_by_bin": null
}
]
JSON Schema (expected_schema.json)¶
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"entry_id": {
"type": "string",
"x-pyarrow": {
"arrow_type": "string"
}
},
"biotype": {
"type": "string",
"x-pyarrow": {
"arrow_type": "dictionary",
"value_type": "string",
"index_type": "int16",
"ordered": false
},
"description": "Controlled biotype term."
},
"priority_tier": {
"anyOf": [
{
"type": "integer",
"minimum": -9223372036854775808,
"maximum": 9223372036854775807,
"x-pyarrow": {
"arrow_type": "dictionary",
"value_type": "int64",
"index_type": "int8",
"ordered": true
}
},
{
"type": "null"
}
]
},
"rate_bucket": {
"type": "string",
"pattern": "^-?\\d{1,6}(\\.\\d{1,2})?$",
"x-pyarrow": {
"arrow_type": "dictionary",
"logical_type": "decimal",
"precision": 8,
"scale": 2,
"value_type": "decimal128",
"index_type": "int8",
"ordered": false
}
},
"xref_hits": {
"type": "array",
"description": "PyArrow map rendered as key/value pairs (JSON object keys must be strings).",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "External database key.",
"x-pyarrow": {
"arrow_type": "string"
}
},
"value": {
"anyOf": [
{
"type": "object",
"properties": {
"db": {
"type": "string",
"x-pyarrow": {
"arrow_type": "string"
}
},
"evalue": {
"anyOf": [
{
"type": "number",
"x-pyarrow": {
"arrow_type": "float64"
}
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"required": [
"db"
],
"x-pyarrow": {
"arrow_type": "struct"
}
},
{
"type": "null"
}
]
}
},
"required": [
"key",
"value"
],
"additionalProperties": false
},
"x-pyarrow": {
"arrow_type": "map",
"keys_sorted": false
}
},
"coverage_by_bin": {
"anyOf": [
{
"type": "array",
"description": "PyArrow map rendered as key/value pairs (JSON object keys must be strings).",
"items": {
"type": "object",
"properties": {
"key": {
"type": "integer",
"minimum": -2147483648,
"maximum": 2147483647,
"x-pyarrow": {
"arrow_type": "int32"
}
},
"value": {
"type": "number",
"x-pyarrow": {
"arrow_type": "float64"
}
}
},
"required": [
"key",
"value"
],
"additionalProperties": false
},
"x-pyarrow": {
"arrow_type": "map",
"keys_sorted": true
}
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"required": [
"entry_id",
"biotype",
"rate_bucket",
"xref_hits"
]
}
CheckM2 Genome Quality and Contig Statistics Table (examples/checkm2_quality_report)¶
Rules: R-SCHEMA-INPUT, R-DIALECT, R-ENVELOPE-ID-TITLE, R-ENVELOPE-CLOSED, R-FIELD-NAME, R-FIELD-ORDER, R-SHAPE-STRUCT, R-SHAPE-LIST, R-SHAPE-MAP, R-SHAPE-DICTIONARY, R-NULL-REQUIRED, R-NULL-OPTIONAL, R-SCALAR-SIGNED-INT, R-SCALAR-UNSIGNED-INT, R-SCALAR-FLOAT, R-SCALAR-DECIMAL, R-SCALAR-STRING, R-META-DESCRIPTION, R-META-X-PYARROW-TYPE, R-SER-ROW-ORDER, R-SER-DECIMAL-STR, R-SER-MAP-ENTRIES
Schema: specs/schema_converters/pyarrow_to_jsonschema/examples/checkm2_quality_report/schema.py
- Run
default:table.py,expected.json
Expected JSON Schema: expected_schema.json
NCBI Datasets Assembly Summary Table (examples/ncbi_assembly_summary)¶
Rules: R-ENVELOPE-ID-TITLE, R-SHAPE-STRUCT, R-SHAPE-LIST, R-SHAPE-DICTIONARY, R-NULL-REQUIRED, R-NULL-OPTIONAL, R-SCALAR-BOOL, R-SCALAR-SIGNED-INT, R-SCALAR-UNSIGNED-INT, R-SCALAR-DECIMAL, R-SCALAR-STRING, R-SCALAR-BINARY, R-SCALAR-DATE-TIME, R-SCALAR-TIMESTAMP, R-META-DESCRIPTION, R-META-X-PYARROW-TYPE, R-SER-ROW-ORDER, R-SER-DECIMAL-STR, R-SER-BASE64, R-SER-TEMPORAL-ISO
Schema: specs/schema_converters/pyarrow_to_jsonschema/examples/ncbi_assembly_summary/schema.py
- Run
default:table.py,expected.json
Expected JSON Schema: expected_schema.json
UniRef Sequence Cluster Inventory Table (examples/uniref_cluster_entry)¶
Rules: R-ENVELOPE-ID-TITLE, R-SHAPE-STRUCT, R-SHAPE-LIST, R-SHAPE-FIXED-LIST, R-SHAPE-MAP, R-NULL-REQUIRED, R-NULL-OPTIONAL, R-SCALAR-UNSIGNED-INT, R-SCALAR-FLOAT, R-SCALAR-STRING, R-SCALAR-BINARY, R-SCALAR-TIMESTAMP, R-META-DESCRIPTION, R-META-X-PYARROW-TYPE, R-META-X-PYARROW-CUSTOM, R-SER-ROW-ORDER, R-SER-BASE64, R-SER-TEMPORAL-ISO, R-SER-MAP-ENTRIES
Schema: specs/schema_converters/pyarrow_to_jsonschema/examples/uniref_cluster_entry/schema.py
- Run
default:table.py,expected.json
Expected JSON Schema: expected_schema.json
3. Errors (Unsupported / Invalid Input)¶
Schema generation raises a subclass of PyArrowToJSONSchemaError (itself a ValueError).
| Code | Exception | Condition | Reason | Fixtures |
|---|---|---|---|---|
| E1 | InvalidPyArrowSchemaError |
Input is not a pa.Schema instance |
Schema generation requires a concrete PyArrow pa.Schema object; bare pa.DataType, pa.Field, or Python mappings are not table schemas. |
e01_non_schema_input.py: Input is a pa.StructType rather than a pa.Schema. |
| E2 | EmptyStructError |
Empty root schema or nested pa.struct([]) (zero fields) |
Arrow tables and Parquet struct columns require at least one named child field. | e02a_empty_root_schema.py: Root pa.schema([]) declares zero fields.e02b_empty_nested_struct.py: Nested pa.struct([]) column declares zero child fields. |
| E3 | DuplicateFieldNameError |
Duplicate field name in schema or pa.struct |
JSON Schema properties is an object mapping keyed by property name and cannot represent duplicate sibling field names. |
e03a_duplicate_root_field.py: Root pa.Schema declares two fields named 'id'.e03b_duplicate_struct_field.py: Nested pa.struct declares duplicate child fields named 'tag'. |
| E4 | NullOnlyTypeError |
Standalone pa.null() field or element type |
A column or container element typed solely as pa.null() carries no value domain; nullable columns must wrap a concrete Arrow value type with nullable=True. |
e04_null_only_field.py: Column uses standalone pa.null() with no value type. |
| E5 | InvalidDecimalTypeError |
Decimal type with negative scale or scale > precision |
Fixed-point decimal string patterns require 0 <= scale <= precision; negative scales or scale > precision have no plain fixed-point string regex. |
e05_negative_scale_decimal.py: Column uses pa.decimal128(6, -2) with negative scale. |
| E6 | UnsupportedTemporalIntervalError |
Duration or calendar interval type (pa.duration, pa.month_day_nano_interval) |
Relative durations and month_day_nano intervals have no lossless canonical scalar representation across JSON Schema validators without losing unit or sign-per-component semantics. |
e06a_duration_field.py: Column uses pa.duration("us").e06b_interval_field.py: Column uses pa.month_day_nano_interval(). |
| E7 | UnsupportedUnionTypeError |
Arrow union type (pa.sparse_union or pa.dense_union) |
Arrow type-code unions lack stable JSON property discriminators and Parquet storage support; union columns must be normalized before schema export. | e07a_sparse_union_field.py: Column uses pa.sparse_union(...).e07b_dense_union_field.py: Column uses pa.dense_union(...). |
| E8 | UnsupportedArrowTypeError |
Unsupported Arrow physical or extension type (run_end_encoded, ExtensionType, etc.) |
Specialized physical encodings such as pa.run_end_encoded and custom pa.ExtensionType subclasses have no general target-independent JSON Schema contract. |
e08a_run_end_encoded_field.py: Column uses pa.run_end_encoded(pa.int32(), pa.string()).e08b_extension_type_field.py: Column uses Arrow extension type pa.uuid(). |
4. Fixture Index¶
Every case directory contains a case.yaml manifest; the test suite and this document are both driven by it.
| Case | Schema | Runs |
|---|---|---|
spec/01_integers_floats_and_booleans |
schema.py |
boundary_values |
spec/02_strings_binary_and_decimals |
schema.py |
default |
spec/03_temporal_types |
schema.py |
default |
spec/04_nullability_and_metadata |
schema.py |
default |
spec/05_metadata_omission_and_overrides |
schema.py |
without_pyarrow_metadata |
spec/06_structs_and_lists |
schema.py |
default |
spec/07_maps_and_dictionaries |
schema.py |
default |
examples/checkm2_quality_report |
schema.py |
default |
examples/ncbi_assembly_summary |
schema.py |
default |
examples/uniref_cluster_entry |
schema.py |
default |