diesel-derive-newtype

A Rust derive macro that makes single-field newtype structs work transparently with Diesel's ORM traits.

Library
Cargo
v2.1.3
55stars
Apache License 2.0

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
49/100Fair
Development Activity40
Maintenance40
Community44
Maturity60
Momentum12

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
68/100Good
Architecture76
Code Quality84
Innovation55
Learning Curve55

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 implements ToSql, FromSql, Queryable, AsExpression (including reference variants), and QueryId for 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_from shorthand - #[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 TryFrom error type is boxed into Box<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::Error messages instead of confusing downstream compile failures, verified by a trybuild-based UI test suite.

Common Use Cases

  • Typed primary keys - wrapping a raw i64/i32 column value in a distinct UserId/OrderId newtype so Diesel query code can’t accidentally compare or swap IDs from different tables.
  • Validated domain values read from the database - using try_from to guarantee a newtype like an Even(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.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search