jpzip
Rust SDK for jpzip — a free, unlimited Japanese postal code (郵便番号) API. 日本の全郵便番号 120,677 件を CDN 配信 JSON から引く Rust SDK。
English | 日本語
jpzip looks up Japanese postal codes (郵便番号) from jpzip.nadai.dev,
a CDN-hosted dataset built from Japan Post's KEN_ALL.csv and KEN_ALL_ROME.csv
normalized to JSON. No registration, no rate limits, no API key.
- 🇯🇵 Complete dataset — 120,677 entries with kanji, kana, romaji, and government codes (JIS X 0401 / 総務省地方公共団体コード)
- ⚡️ Async + cached —
tokio+reqwest, L1 LRU + optional L2 persistent cache;preloadto serve lookups without per-request network round-trips - 🛡️ Resilient — 3-attempt retry with exponential backoff on 5xx / network failures
- 🪶 Lightweight — pure-Rust TLS (
rustls), noopenssl-sys, no C toolchain at build time - 🆓 Free forever — backed by Cloudflare Pages' free tier (no billing axis exists)
- 🔌 Drop-in — same API surface across every jpzip SDK
Requirements
Rust 1.75+ (edition 2021), tokio runtime.
Install
Or in Cargo.toml:
[]
= "0.1"
= { = "1", = ["full"] }
Quick Start
async
Romaji and government codes are included on the same entry:
let e = lookup.await?.unwrap;
println!;
// Output: Kanagawa Ken Yokohama Shi Naka Ku Minatocho
println!;
// Output: 14 14104
Use Cases
Zipcode lookup HTTP endpoint (axum)
use ;
use ZipcodeEntry;
async
async
Zipcode lookup HTTP endpoint (actix-web)
use ;
async
async
Batch validation
let all = lookup_all.await?; // entire dataset in memory (~37 MiB JSON)
for zip in csv_zipcodes
Serve lookups from cache (BYO L2 backend)
The dataset is partitioned into 948 three-digit prefix buckets. The default
L1 (100 entries) keeps the hottest buckets; to cache the whole dataset, pair
preload("all") with an L2 cache or raise memory_cache_size above 948.
use Arc;
use ;
# async
API Reference
Full docs on docs.rs/jpzip.
Functions (crate-level, share a default JpzipClient)
| Function | Description |
|---|---|
lookup(zipcode) |
Look up a single 7-digit zipcode. Returns Ok(None) if not found or malformed (no network call for malformed input). |
lookup_group(prefix) |
Look up by 1-, 2-, or 3-digit prefix. 1-digit fetches /g/{d}.json; 3-digit fetches /p/{ddd}.json; 2-digit fans out into 10 parallel 3-digit fetches and merges. |
lookup_all() |
Fetch entire dataset (120k entries, ~37 MiB) in parallel across /g/0..9.json. |
get_meta() |
Dataset version, generated-at, per-prefecture counts, spec version. Cached until refresh. |
preload(scope) |
Warm L1 (and L2 when configured) for "all" or a specific prefix. |
is_valid_zipcode(s) |
Pure syntax check (^\d{7}$) — no network. |
All async; all return Result<_, jpzip::Error>.
JpzipClient (advanced)
JpzipClient::builder() returns a configurable instance; required for L2 caching, custom HTTP client, alternate base URL, or multiple isolated caches:
use Duration;
use JpzipClient;
let client = builder
.base_url
.http_client
.memory_cache_size // L1 capacity in prefix buckets, default 100
.cache // optional L2 (Arc<dyn Cache>)
.on_spec_mismatch
.build;
JpzipClient exposes lookup / lookup_group / lookup_all / get_meta / preload plus:
| Method | Description |
|---|---|
client.refresh() |
Wipe L1 (and L2 when configured) and forget the cached meta. |
When get_meta observes that /meta.json's version has changed since the last successful fetch, L1 and L2 are cleared automatically — call get_meta periodically to pick up dataset rollovers.
Errors
jpzip::Error is a single thiserror-derived enum:
| Variant | When |
|---|---|
Error::Http(reqwest::Error) |
Transport-level failure after retries are exhausted. |
Error::Status { url, status } |
Non-2xx HTTP (404 is mapped to Ok(None) instead). |
Error::Parse(serde_json::Error) |
JSON shape did not match the dataset schema. |
Error::InvalidPrefix(String) |
lookup_group / preload received a prefix that is not 1-3 digits. |
Error::Cache(Box<dyn Error + Send + Sync>) |
Bubbled up from your Cache implementation. |
Transient network failures and 5xx responses are retried up to 3 attempts (initial + 2 retries) with exponential backoff sleeps of 400 ms and 800 ms. 4xx responses other than 404 are returned immediately.
Cache trait
Bring your own L2 backend (file, sled, Redis, KV, etc.):
use async_trait;
use ;
Pass Arc<dyn Cache> to JpzipClient::builder().cache(...). Keys are the full prefix-bucket URLs (e.g. https://jpzip.nadai.dev/p/231.json); values are raw JSON bytes.
Why jpzip?
| jpzip | jpostcode_rs | kenall-rs | zipcloud API | |
|---|---|---|---|---|
Romaji (Yokohama Shi) |
✅ | ❌ | ⚠️ via paid plan | ❌ |
| Government codes (JIS / 総務省) | ✅ | ⚠️ JIS only | ✅ | ❌ |
| No manual CSV download | ✅ | ✅ Embedded | ✅ | ✅ |
| Monthly updates | ✅ Auto | ❌ Bumped on crate release | ✅ | ✅ |
| Offline after preload | ✅ | ✅ (always) | ❌ | ❌ |
| No API key | ✅ | ✅ | ❌ Required | ✅ |
| Rate-limit-free | ✅ | ✅ | ⚠️ Plan-gated | ⚠️ Discouraged |
Async (tokio) |
✅ | ❌ Sync | ✅ | n/a |
| L1 + pluggable L2 cache | ✅ | n/a (in-binary) | ❌ | ❌ |
jpostcode_rs is the right choice if you want zero-network, zero-config lookups and don't need romaji; binary size grows with the dataset. kenall-rs wraps the commercial KENALL service. jpzip sits between them: HTTP-fetched (so the dataset stays current without re-publishing the crate), preloadable (so production traffic doesn't depend on the CDN per-request), and free.
Other Languages
Same API surface across all SDKs:
Go · TypeScript · Python · Ruby · PHP · Swift · Dart
Resources
- Website — https://jpzip.nadai.dev
- Protocol spec — jpzip/spec
- Data ETL — jpzip/data
- MCP server — jpzip/mcp — use jpzip from Claude / ChatGPT / Cursor
Keywords
japanese postal code, japan zipcode, 郵便番号, KEN_ALL, KEN_ALL_ROME, address validation, japan address api, postal code lookup rust, rust japanese address, async zipcode crate, JIS X 0401, 総務省地方公共団体コード