diesel-derive-newtype
A Rust derive macro that makes single-field newtype structs work transparently with Diesel's ORM traits.
Repository Health
Technical Analysis
diesel-derive-newtype exposes a single custom-derive macro, DieselNewType, that implements ToSql, FromSql, FromSqlRow, Queryable, AsExpression, and QueryId for a single-field tuple struct (a Rust newtype). Applying it to a type like struct MyId(i64) lets that type be used anywhere Diesel expects the underlying SQL type — as a column value, in Identifiable/Queryable structs, and in query expressions — without hand-writing five trait implementations per newtype.
The crate also supports an opt-in #[diesel_newtype(try_from = InnerType)] (or bare try_from) attribute for newtypes with invariants: instead of wrapping the raw database value directly, the read path deserializes the intermediate type and calls .try_into(), so an invalid value in the database surfaces as a conversion error rather than silently producing an invalid instance. The write path always serializes the inner field directly and is unaffected by try_from.
It is a small, single-purpose crate (~450 lines) targeting Diesel 2.x, versioned to track Diesel’s own major versions, with an MSRV pinned via rust-version and resolver = “3” for MSRV-aware dependency resolution in CI.
What You Get
#[derive(DieselNewType)]- a single derive that implementsToSql,FromSql,Queryable,AsExpression(including reference variants), andQueryIdfor a single-field tuple struct.- Invariant-preserving reads -
#[diesel_newtype(try_from = InnerType)]deserializes an intermediate type and.try_into()s it, so invalid database values fail to construct rather than silently producing a bad instance. - Bare
try_fromshorthand -#[diesel_newtype(try_from)]defaults the intermediate type to the newtype’s own field type, covering the common case with less boilerplate. - Preserved conversion errors - the
TryFromerror type is boxed intoBox<dyn std::error::Error + Send + Sync>rather than stringified, so callers keep a typed error. - Pointed compile errors - misuse (enums, unit structs, multi-field structs, unknown attribute keys, empty attributes) produces span-targeted
syn::Errormessages instead of confusing downstream compile failures, verified by atrybuild-based UI test suite.
Common Use Cases
- Typed primary keys - wrapping a raw
i64/i32column value in a distinctUserId/OrderIdnewtype so Diesel query code can’t accidentally compare or swap IDs from different tables. - Validated domain values read from the database - using
try_fromto guarantee a newtype like anEven(i32)or a bounded numeric type can only ever hold a value that satisfies its invariant, even when reading rows written by another process. - Reducing per-newtype boilerplate in larger schemas - applying the derive across many ID and value-object types in a Diesel-backed application instead of maintaining hand-written trait impls for each.
- Working around Diesel’s Rust-type strictness - using a newtype as a drop-in for its wrapped SQL type in
Identifiable,Queryable, and expression contexts without extra glue code.
Under The Hood
Architecture
The crate is a single ~450-line src/lib.rs exposing one proc-macro entry point, diesel_new_type, wired to #[proc_macro_derive(DieselNewType, attributes(diesel_newtype))]. It parses the target with syn, validates via validate_wrapped_type that the input is a single-field tuple struct (rejecting enums, unions, unit structs, and multi-field structs with span-pointed errors), then parses the optional #[diesel_newtype(try_from = ...)] attribute into a TryFromAttr enum (None/Bare/InnerType). The five trait implementations are generated by dedicated gen_* functions (gen_tosql, gen_asexpressions, gen_from_sql, gen_queryable, gen_query_id) and assembled by expand_sql_types, with the shared read-path construction logic factored into gen_build_from_inner so the try_from-vs-direct-wrap semantics live in exactly one place for both FromSql and Queryable. All generated impls are wrapped in an anonymous const _: () = { ... }; block to avoid polluting the invoking module’s namespace. Changing expand_sql_types directly changes what trait impls every downstream newtype receives, making it the crate’s single point of leverage.
Tech Stack
A standard Rust proc-macro crate built on syn 2.0.10, quote 1.0.23, and proc-macro2 1.0.51. Dev-dependencies are diesel 2.1.0 (sqlite feature, default-features off) for integration testing and trybuild 1.0 for compile-error snapshot testing. Edition 2018, with rust-version = "1.84.0" pinned to track Diesel’s own MSRV and resolver = "3" enabling MSRV-aware dependency resolution so a fresh CI resolve doesn’t pull in deps requiring a newer toolchain. CI runs via GitHub Actions (test.yml) across the stable/beta matrix plus a dedicated 1.84.0 job that runs the ui feature’s trybuild suite, since its pinned .stderr snapshots drift on other toolchain versions.
Code Quality
Error handling is explicit throughout, using syn::Result and span-targeted syn::Error messages rather than panics — including a dedicated check for the confusing bare #[diesel_newtype]/#[diesel_newtype()] case. Testing is extensive for a crate this size: tests/db-roundtrips.rs exercises real sqlite round-trips, tests/try-from.rs covers both the explicit and bare try_from forms, tests/should-not-compile.rs/compile-fail.rs document intentional compile failures, and tests/ui/ holds nine trybuild fixtures with pinned .stderr snapshots (unit struct, two fields, zero fields, enum, named field, duplicate try_from, unknown key, bare attr, empty parens) gated behind a ui cargo feature with a documented regeneration command. A #[expect(clippy::large_enum_variant)] annotation indicates clippy runs in CI. Naming is consistent and comments explain non-obvious design choices (e.g. why the write path needs no Clone/Into bound).
API Design
The public surface is intentionally minimal: one derive macro plus one attribute. The zero-config path (#[derive(DieselNewType)]) covers the common case with no boilerplate, and the opt-in try_from/try_from = Type attribute layers in validation only when needed, defaulting sensibly to the field’s own type in the bare form. Error messages are written for a downstream user encountering a misuse, not just for the macro author, which is a notable DX investment for a crate this size. It is narrowly scoped to Diesel newtypes rather than a general derive-macro toolkit, which keeps its API surface small and easy to reason about but limits it to one ecosystem-specific niche.