A reader and writer for the Apache Parquet file format, optimized for minimal dependencies and great performance. Available as a Java library and a command-line tool.
Hardwood gives applications fast and efficient support for reading and writing Parquet, without pulling in Hadoop, Avro, or the wider parquet-java dependency tree. It is built to be:
- Light-weight: Zero transitive dependencies beyond optional compression libraries (Snappy, ZSTD, LZ4, Brotli)
- Fast: Hardwood aims to be the fastest Parquet reader and writer for the JVM — see the read benchmarks
- Complete: Read and write support for flat and nested schemas, every logical type, every primitive type in current use, and the encodings and codecs in current use, with new format additions tracked as they land
- Scalable: Hardwood is multi-threaded at the core, pages are decoded in parallel, with cross-file prefetching for multi-file reads
- Embeddable: The Hardwood library can be used in GraalVM native binaries; WASM support coming soon (preview)
- Agent-friendly: Hardwood's CLI comes with a skill which lets your agents inspect and analyse Parquet files
- Compatible: Supports all Parquet files which the canonical
parquet-javalibrary supports, and a drop-in shim module eases migration from it, with documented divergences where Hardwood applies stricter semantics (e.g. SQL three-valuednotEq)
Besides the core library, Hardwood provides a ready-to-use CLI for inspecting and analysing Parquet files, including an interactive TUI for exploring a file's schema, row groups, pages, and data.
Latest version: 1.1.0.Beta1, 2026-08-31
Full documentation is available at hardwood.dev.
<dependency>
<groupId>dev.hardwood</groupId>
<artifactId>hardwood-core</artifactId>
<version>1.1.0.Beta1</version>
</dependency>Here's how you read a file with the row-based API:
import dev.hardwood.InputFile;
import dev.hardwood.reader.ParquetFileReader;
import dev.hardwood.reader.RowReader;
try (ParquetFileReader fileReader = ParquetFileReader.open(InputFile.of(path));
RowReader rowReader = fileReader.rowReader()) {
while (rowReader.hasNext()) {
rowReader.next();
long id = rowReader.getLong("id");
String name = rowReader.getString("name");
LocalDate birthDate = rowReader.getDate("birth_date");
Instant createdAt = rowReader.getTimestamp("created_at");
}
}And here's how you write a file:
import dev.hardwood.OutputFile;
import dev.hardwood.writer.ParquetFileWriter;
import dev.hardwood.writer.RowWriter;
try (ParquetFileWriter writer = ParquetFileWriter.create(OutputFile.of(path), schema)) {
RowWriter rows = writer.rowWriter();
for (Person person : people) {
rows.writeRow(row -> row
.setLong("id", person.id())
.setString("name", person.name())
.setDate("birth_date", person.birthDate()));
}
}See the Getting Started guide for detailed setup instructions.
- Local files (memory-mapped via
InputFile.of(Path)) may be arbitrarily large; each individual column chunk must be at most 2 GB of compressed data. - In-memory (
InputFile.of(ByteBuffer)) and object-store sources are limited to 2 GB per file. Split larger datasets across multiple files and read them withHardwood.openAll(...)orParquetFileReader.openAll(...). - Writing is under active development as of Hardwood 1.1, and is documented in the development docs until 1.1 is released. It targets local files via
OutputFile.of(Path); output to object storage is coming soon.
| Document | Purpose |
|---|---|
| ARCHITECTURE.md | High-level architecture and module layout. |
| CONTRIBUTING.md | How to contribute: workflow, commit format, PR expectations. |
| ROADMAP.md | Implementation status, roadmap, and milestones. |
| NATIVE_BUILD.md | How the GraalVM native CLI build works. |
| PERFORMANCE.md | Benchmark results and how to run performance tests. |
| TESTING.md | Manual testing recipes (e.g. S3 via s3proxy). |
| RELEASING.md | Release process. |
See hardwood.dev for a list of articles, talks, and podcasts about Hardwood.
Contributions are welcome! See CONTRIBUTING.md for the full guide — how to find work, the issue-first workflow, commit message format, and PR expectations.
- File bugs and feature requests on the issue tracker.
- Ask questions or discuss ideas on GitHub Discussions.
- Looking for something to work on? Browse
good first issueandhelp wanted.
LLM-assisted contributions are welcome, but vibe coding — accepting AI-generated changes without understanding them — is not. The aspiration is a high-quality, maintainable, performant, safe codebase.
See ROADMAP.md for the detailed implementation status, roadmap, and milestones.
This project requires Java 25 or newer for building (to create the multi-release JAR with Java 22+ FFM support). The resulting JAR runs on Java 21+ (libdeflate support requires Java 22+).
Docker must be running for a full build to succeed, as the integration tests use Testcontainers to spin up services (e.g. an S3 proxy). Without Docker, build with -DskipITs to run the unit test suite on its own.
It comes with the Apache Maven wrapper, i.e. a Maven distribution will be downloaded automatically, if needed.
Run the following command to build this project:
./mvnw clean verifyOn Windows, run the following command:
mvnw.cmd clean verifyPass the -Dquick option to skip all non-essential plug-ins and create the output artifact as quickly as possible:
./mvnw clean verify -DquickRun the following command to format the source code and organize the imports as per the project's conventions:
./mvnw process-sourcesThe hardwood CLI compiles to a GraalVM native binary. Requires GraalVM (Java 25+) installed locally — install via SDKMAN:
sdk install java 25.0.2-graalceThen build the cli module and its dependencies:
./mvnw -Dnative package -pl cli -amThe resulting binary is at cli/target/hardwood-cli.
See NATIVE_BUILD.md for the full build guide — obtaining a Linux binary, the Docker image, and how the native build works (codec handling, build arguments).
The documentation site can be previewed locally using Docker:
# Build the image (once, or after changing requirements.txt)
docker build -t hardwood-docs docs/
# Serve locally with hot reload — preview at http://127.0.0.1:8000
docker run --rm -p 8000:8000 -v "$(pwd):/repo" hardwood-docs
# Build static site (output in docs/site/)
docker run --rm -v "$(pwd):/repo" hardwood-docs build -f docs/mkdocs.ymlThe serve command polls the mounted repository, so edits to docs/content,
docs/overrides, docs/hooks, and docs/mkdocs.yml rebuild the site and
refresh the open browser tab. Changes to docs/requirements.txt require a
rebuild of the image.
A Docker Compose set-up is provided for running Claude Code with all build dependencies (Java 25, Maven, gh) pre-installed.
GH_TOKEN=<your-token> docker compose run --rm claudeSet GH_TOKEN to a GitHub personal access token so that Claude Code can interact with issues and pull requests. The project directory is mounted into the container at /workspace, and Claude Code configuration is persisted in a named volume across sessions.
See RELEASING.md.
To generate an API change report across all published modules (hardwood-core, hardwood-avro, hardwood-s3, hardwood-aws-auth):
tools/api-report.sh <PREVIOUS_VERSION> # HEAD (snapshot) vs <PREVIOUS_VERSION>
tools/api-report.sh <PREVIOUS_VERSION> <LATER_VERSION> # compare two published versionsIn the default (single-argument) form the script installs the snapshot jars and compares HEAD against <PREVIOUS_VERSION>. With both arguments the build step is skipped and both sides are resolved from the Maven repository — useful to diff arbitrary released pairs (e.g. Beta2 against CR1).
The script writes a unified target/japicmp/api-report.diff (text) and target/japicmp/api-report.html (one document with a TOC linking to per-module sections). Per-module reports also land under target/japicmp/<artifactId>/ (HTML / Markdown / XML / diff). Internal packages (dev.hardwood.internal) are excluded. This is run automatically during releases.
See PERFORMANCE.md for benchmark results and instructions on running performance tests.
This code base is available under the Apache License, version 2.