Skip to content

Typed Accessors

RowReader and the nested PqStruct / PqList / PqMap flyweights decode each column to its logical-type Java representation through typed accessor methods. For the task-oriented walkthrough, see Read Row by Row.

Try it yourself

Want to run it or explore the capabilities yourself? The Typed Accessors example decodes dates, times, UTC and local timestamps, decimals, UUIDs, intervals, FLOAT16, and JSON/BSON to their natural Java types.

Accessor type mapping

All accessors are available in two forms — name-based (getInt("column_name")) and index-based (getInt(fieldIndex)); see Index-based access.

Method Physical type Logical type Java type
getBoolean BOOLEAN none boolean
getInt INT32 any int
getLong INT64 any long
getFloat FLOAT
FIXED_LEN_BYTE_ARRAY(2)
none
FLOAT16
float
getDouble DOUBLE none double
getBinary BYTE_ARRAY
FIXED_LEN_BYTE_ARRAY
INT96
any byte[]
getString BYTE_ARRAY STRING, ENUM, JSON, or none String
getDate INT32 DATE LocalDate
getTime INT32
INT64
TIME LocalTime
getTimestamp INT64
FIXED_LEN_BYTE_ARRAY(12)
INT96
TIMESTAMP(isAdjustedToUTC = true); none on INT96 Instant
getLocalTimestamp INT64
FIXED_LEN_BYTE_ARRAY(12)
TIMESTAMP(isAdjustedToUTC = false) LocalDateTime
getDecimal INT32
INT64
BYTE_ARRAY
FIXED_LEN_BYTE_ARRAY
DECIMAL BigDecimal
getUuid FIXED_LEN_BYTE_ARRAY(16) UUID UUID
getInterval FIXED_LEN_BYTE_ARRAY(12) INTERVAL PqInterval
getStruct group none PqStruct
getList group LIST PqList
getMap group MAP PqMap
getVariant group of two BYTE_ARRAY VARIANT PqVariant
isNull any any boolean

PqInterval is a record with three long components, months(), days() and milliseconds(), each an unsigned 32-bit value in the range [0, 4_294_967_295]. The components are independent and not normalized.

Physical accessors

getInt, getLong and getBinary read the stored value of any column their physical type holds, whatever it is annotated with. getInt reads a DATE, a TIME(MILLIS) and an INT32 DECIMAL as well as a bare INT32; getLong reads an INT64 TIMESTAMP, a TIME(MICROS) or TIME(NANOS) and an INT64 DECIMAL; getBinary reads every BYTE_ARRAY, FIXED_LEN_BYTE_ARRAY and INT96 column, including a DECIMAL, a UUID, an INTERVAL, a FLOAT16, a TIMESTAMP over FIXED_LEN_BYTE_ARRAY(12), a BSON and a GEOMETRY or GEOGRAPHY payload. Use them to skip the logical-type decode — see Reading the physical value.

getValue on a signed INT(8) or INT(16) column returns a Byte or a Short. A file can store a value outside the range its annotation states: getValue keeps the low 8 or 16 bits of such a value, so a stored 1000 reads as the Byte -24, while getInt returns it as stored. On an unsigned INT(8) or INT(16) column getValue returns the stored Integer. Filter predicates compare the value getInt returns.

Generic accessors

When the column type is not known ahead of time, the generic accessors return values decoded to their logical-type representation:

  • RowReader.getValue(name) / getValue(index): Integer / Long / String / LocalDate / LocalTime / Instant / BigDecimal / UUID / PqInterval / PqVariant / nested PqStruct / PqList / PqMap, with byte[] for un-annotated BYTE_ARRAY / FIXED_LEN_BYTE_ARRAY columns.
  • PqStruct.getValue(name): same decoded mapping for nested struct fields.
  • PqMap.Entry.getKey() / getValue(): same decoded mapping for map keys and values.
  • PqList.get(index) / PqList.values(): same decoded mapping for list elements.

The generic accessors, PqList.iterator included, return an un-annotated BYTE_ARRAY as byte[], since it may hold arbitrary binary payloads (Protobuf, WKB, custom encodings); call getString when the column is known to hold UTF-8 text from an older writer that omitted the STRING annotation.

A parallel getRawValue family (RowReader.getRawValue, PqStruct.getRawValue, PqMap.Entry.getRawKey / getRawValue, PqList.getRaw / rawValues) returns the boxed physical value. Nested groups (struct / list / map / variant) have no distinct raw form and are returned through their typed flyweight (PqStruct / PqList / PqMap / PqVariant) in both modes.

Typed accessors on PqList and PqMap.Entry

PqList has strings() / dates() / times() / timestamps() / decimals() / uuids() / intervals() / floats() / booleans(), each returning List<T>, and PqMap.Entry the matching getStringValue() / getDateValue() / getIntervalValue() / etc., which avoid the boxed Object return of getValue().

PqMap.Entry's typed key accessors are getStringKey() / getIntKey() / getLongKey() / getBinaryKey(). Other key types (DATE / TIME / TIMESTAMP / DECIMAL / UUID) are read through getKey() (decoded) and getRawKey() (raw).

PqList.ints() / longs() / doubles() return PqIntList / PqLongList / PqDoubleList, which expose PrimitiveIterator.OfInt / OfLong / OfDouble, int get(int), and int[] toArray() without boxing. For nested list<list<int>> (or <long> / <double>), iterate the outer list via lists() and call ints() / longs() / doubles() on each inner PqList.

Text columns

getString reads a column whose stored bytes are the UTF-8 encoding of a string: a BYTE_ARRAY annotated STRING, ENUM or JSON, or one carrying no annotation, which older writers used for text before the annotation existed. On any other column — a DECIMAL, a UUID, an INTERVAL, a FLOAT16, a BSON, a GEOMETRY or GEOGRAPHY, an INT96, a fixed-width FIXED_LEN_BYTE_ARRAY, or a column not held as bytes at all — it throws IllegalArgumentException naming the column and its type; read those through getBinary or the accessor for their annotation.

The same columns are the ones a String filter literal applies to; see Query Controls. The rule holds for every string accessor: getString on RowReader and PqStruct, strings() on PqList, getStringKey / getStringValue on PqMap.Entry, and ColumnReader.getStrings.

Where the refusal falls depends on the accessor. The per-value ones — getString, getStringKey, getStringValue — answer the null test first, so a null value reads as null whatever its column holds and the refusal comes on the first present value. PqList.strings() and ColumnReader.getStrings() ask about the column once for a whole run of values, and refuse one that does not hold text whatever its values are.

Null handling

Primitive accessors (getInt, getLong, getFloat, getDouble, getBoolean) throw NullPointerException if the field is null — always check isNull() first. Object accessors (getString, getDate, getTimestamp, getLocalTimestamp, getDecimal, getUuid, getInterval, getStruct, getList, getMap) return null for null fields.

A column annotated with the NULL logical type (e.g. PyArrow's pa.null()) holds a null value at every row: column.logicalType() returns LogicalType.NullType, isNull(name) is always true, and the object accessors return null.

Type mismatches

Requesting the wrong type for a column (e.g. getInt on a LONG column, getDate on a STRING column) is a programming error; the call fails at runtime with an unchecked exception. The specific exception type is unspecified and may change between releases — do not catch it as part of normal control flow. Reading a column that does not hold text as a string is specified: the string accessors listed under Text columns throw IllegalArgumentException, as their @throws records. If the column type isn't known statically, check it up front via reader.getFileSchema().getColumn(name) and inspect the returned ColumnSchema's type() / logicalType() — see Inspect File Metadata.

A column whose annotation its physical type cannot carry is not an error. FLOAT16 is defined as a two-byte payload, so a FLOAT16 column that declares three bytes is invalid. So are a TIME(MILLIS) or an INT(8) on an INT64, and a DECIMAL(12, 2) on an INT32, which holds at most nine digits. The format tells readers to ignore such an annotation rather than reject the file, so Hardwood drops it, and drops one it does not recognize at all. The column is then reported and read as its physical type.

getFileSchema() reports no logical type for it, getValue returns the physical value, and the physical accessors work. A logical accessor fails as it would on any unannotated column of that type. Its min / max statistics prune nothing (see When statistics are ignored), and a logical-type predicate on the column is rejected at reader creation.

The getTimestamp / getLocalTimestamp pair is split along the column's isAdjustedToUTC flag: getTimestamp requires isAdjustedToUTC = true and returns Instant; getLocalTimestamp requires isAdjustedToUTC = false and returns LocalDateTime. Calling the wrong one for a column throws IllegalStateException naming the column and the actual flag value. If the kind isn't known statically, branch on ((LogicalType.TimestampType) column.logicalType()).isAdjustedToUTC() before the accessor call, or use the generic getValue accessor, which returns Instant or LocalDateTime per the column's flag. For why the pair is split, see Timestamp Semantics.

The TIME logical type also carries an isAdjustedToUTC flag, but LocalTime has no zone of its own, so getTime returns LocalTime either way and the flag is informational — inspect ((LogicalType.TimeType) column.logicalType()).isAdjustedToUTC() if the distinction matters to your application.

FLOAT16 columns

getFloat accepts FLOAT16 columns (FIXED_LEN_BYTE_ARRAY(2) annotated with the FLOAT16 logical type) and decodes the 2-byte IEEE 754 half-precision payload to a single-precision float. The widening is lossless — half-precision NaN, ±Infinity, and signed zero round-trip cleanly, and the original NaN bit pattern is preserved (the Parquet spec does not canonicalize NaNs on write). Use Float.isNaN(value) for NaN checks rather than equality.

Timestamps over FIXED_LEN_BYTE_ARRAY(12)

A TIMESTAMP column is stored in an INT64 or in a FIXED_LEN_BYTE_ARRAY(12). The 12 bytes hold a signed two's complement little-endian 96-bit count of the annotation's time unit since the Unix epoch, which spans the years 0001 to 9999 at every unit, nanoseconds included.

getTimestamp, getLocalTimestamp, getValue and the PqList / PqStruct / PqMap flyweights read both carriers alike and return the same Instant or LocalDateTime. getBinary, getRawValue and ColumnReader return the 12 stored bytes.

A stored count outside the range of Instant or LocalDateTime, about a billion years either side of the epoch, throws DateTimeException naming the stored bytes; getBinary reads it.

Legacy INT96 timestamps

Parquet files written by older versions of Apache Spark and Hive store timestamps in the deprecated INT96 physical type without a TIMESTAMP logical type annotation. getTimestamp decodes INT96 to an Instant. The generic getValue accessor and the PqList / PqStruct / PqMap flyweights return the same Instant, at the top level and in every nested position.

Legacy converted-type annotations

Writers predating the modern logical-type union (older parquet-mr / Hive / Impala / Spark) annotate primitive columns with only a legacy converted_type and no logicalType. Hardwood promotes each one to its logical type, so the column decodes through the normal typed accessor with no caller-side opt-in:

converted_type Accessor Java type
UTF8 getString String
JSON getString String
ENUM getString String
BSON getBinary byte[]
DATE getDate LocalDate
DECIMAL getDecimal BigDecimal
TIME_MILLIS, TIME_MICROS getTime LocalTime
TIMESTAMP_MILLIS, TIMESTAMP_MICROS getTimestamp Instant
INT_8, INT_16, INT_32, INT_64 getValue Byte / Short / Integer / Long
UINT_8, UINT_16, UINT_32, UINT_64 getValue Integer / Long (raw two's-complement bit pattern)
INTERVAL getInterval PqInterval

TIME_* columns decode to a UTC-normalized LocalTime time-of-day and TIMESTAMP_* columns to a UTC-normalized Instant, matching the parquet-format backward-compatibility rule for these annotations. Unsigned columns preserve the stored bit pattern — reinterpret with Integer.toUnsignedLong / Long.toUnsignedString for the unsigned magnitude. When a file carries both a converted_type and a modern logicalType, the logicalType takes precedence, except where the logicalType is UNKNOWN: then the converted_type decides. parquet-java writes INTERVAL columns in that form, as converted_type = INTERVAL beside logicalType = UNKNOWN, and they read as INTERVAL.

The MAP group annotation has a legacy form too: some older parquet-mr / Hive / Impala files annotate only the inner repeated key_value group with MAP_KEY_VALUE and leave the outer group unannotated. Hardwood recognizes this form as a map, so getMap returns a PqMap and the group's SchemaNode.GroupNode.isMap() reports true (with logicalType() returning a MapType), exactly as for the modern MAP annotation.

Legacy list encodings

Lists predate the modern LIST-annotated three-level group and appear in older files in two legacy forms. Hardwood reads both as lists, so getList returns a PqList with no caller-side opt-in:

  • Unannotated repeated field. A REPEATED field carrying no LIST/MAP annotation is a required list of required elements whose element type is the field itself. A repeated primitive reads as a list of scalars (getList("foo").ints()); a repeated group reads as a list of structs (getList("foo").structs()). A repeated group whose only child is a repeated MAP_KEY_VALUE group reads as a list of maps (getList("attrs").maps()), and getFileSchema() reports the group as a map. A repeated group outside a LIST or MAP group that carries an annotation of its own, such as MAP_KEY_VALUE, LIST or MAP, reads as the same group without that annotation: Hardwood drops the annotation, logging a warning, and getFileSchema() reports the group as it would the unannotated one. Such a group annotated VARIANT keeps its annotation in getFileSchema(); building a reader whose projection includes one of its columns raises an UnsupportedOperationException naming the group, and reads of the other columns succeed. A filter on the group or its columns is rejected as on any repeated column (see Query Controls).
  • Two-level LIST group. A LIST-annotated group whose repeated child is the element directly, with no intermediate list wrapper. Hardwood applies the parquet-format backward-compatibility rules to resolve the element: a repeated group with multiple fields, a repeated group named array or <list>_tuple, or a repeated group whose single field is itself repeated is the element struct; otherwise the repeated group's single child is the element.