# Nutype makes an invalid Username unconstructible, including through serde

> Nutype is an MIT licensed Rust proc macro that turns the newtype pattern into a guarantee: sanitizers and validators declared on the type make every construction path enforce them, and an error enum is generated from the rules you wrote. Character-based and UTF-16 length validators exist for the same reason.

**greyblake/nutype** — Rust newtype with guarantees  🇺🇦 🦀

- Repository: https://github.com/greyblake/nutype
- Stars: 1,777 · Forks: 37
- Language: Rust
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/greyblake-nutype

## One declaration, one constructor, one generated error enum

Nutype is a proc macro that allows adding extra constraints like sanitization and validation to the regular newtype pattern. The generated code makes it impossible to instantiate a value without passing the checks, and the README emphasises that this works even with `serde` deserialization.

That last clause is the whole point of the crate, and it is worth being blunt about why. Nutype states that its generated code keeps these checks in place even during `serde` deserialization.

The quick start shows the whole surface:

```rust
#[nutype(
    sanitize(trim, lowercase),
    validate(not_empty, len_char_max = 20),
    derive(Debug, PartialEq, Clone),
)]
pub struct Username(String);
```

Two things are generated that you did not write. A constructor, `::try_new()`, and an error enum, `UsernameError`, with one variant per violated rule. The README demonstrates both:

```rust
assert_eq!(
    Username::try_new("   FooBar  ").unwrap().into_inner(),
    "foobar"
);
```

```rust
assert_eq!(
    Username::try_new("   "),
    Err(UsernameError::NotEmptyViolated),
);
```

Note the sanitizers run before the validators. `" FooBar "` trims and lowercases to `"foobar"` and passes, while a string of only whitespace trims to empty and then fails `not_empty`. If you wrote the rules in the other order you would get different results, and that ordering is a property of the macro rather than something the syntax expresses explicitly.

The generated error enum is the part that pays for itself. Matching on `NotEmptyViolated` versus `LenCharMaxViolated` is possible because the variants exist, so an API can distinguish a malformed field from a too-long one.

## Four inner type families, and the available rules differ per family

The README is explicit that available sanitizers, validators and derivable traits are determined by the inner type, and it splits them into four categories.

String supports only the `String` owned type at the moment. That is a real constraint: a `&str` newtype is not supported, so a type that should borrow cannot use Nutype as written. The examples directory does include `str_reference`, so there is some related work, but the README's statement stands as the documented limit.

Integer covers the full set of primitive widths and signs: `u8`, `u16`, `u32`, `u64`, `u128`, `i8`, `i16`, `i32`, `i64`, `i128`, `usize` and `isize`.

Float covers `f32` and `f64`.

Decimal is `rust_decimal::Decimal` and requires the `rust_decimal` feature, so it is opt-in.

The last category is anything else, and the examples directory shows what it covers: `any_generics`, `any_arbitrary`, `string_arbitrary`, `integer_arbitrary`, `float_arbitrary`, `float_sortable`, `integer_bounded`, `no_std_example`, `valuable_example` and `serde_complex`. Arbitrary integration, generics and `no_std` support are separate examples rather than claims in the README text, which is the right place for them.

Four string sanitizers are documented: `trim` for leading and trailing whitespace, `lowercase`, `uppercase`, and `with` for a custom function or closure receiving `String` and returning `String`, with the example given as truncating to five characters. Sanitization is deliberately small; anything richer is the `with` escape hatch.

String validators are richer than the sanitizers, and the length rules carry the detail that matters most. `len_char_min` and `len_char_max` are measured in chars, not bytes. `len_utf16_min` and `len_utf16_max` are measured in UTF-16 code units, which the README flags as useful for JavaScript interop.

That distinction has a concrete consequence. A validator written with `len_char_max = 20` accepts a string whose UTF-16 length exceeds 20 if it contains characters outside the basic multilingual plane, because each such character is two UTF-16 units. If your length limit is enforced elsewhere in a JavaScript system, `len_utf16_max` is the validator that matches it and `len_char_max` is the one that does not. Having both available in the same crate is a deliberate accommodation of that bug class.

## Regex validation needs two things, and they are easy to miss

The regex validator has documented requirements, and it is worth quoting them because both are easy to omit.

The `regex` feature of `nutype` must be enabled, and you must explicitly include `regex` as a dependency. A feature flag alone does not bring the crate in; the README is clear that the dependency is separate.

Three ways of supplying the pattern are shown. In place, as a string literal:

```rs
#[nutype(validate(regex = "^[0-9]{3}-[0-9]{3}$"))]
pub struct PhoneNumber(String);
```

Or as a compiled static with `std::sync::LazyLock`:

```rs
use regex::Regex;

static PHONE_NUMBER_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new("^[0-9]{3}-[0-9]{3}$").unwrap());

#[nutype(validate(regex = PHONE_NUMBER_REGEX))]
pub struct PhoneNumber(String);
```

Or with the `lazy_static` macro, using its `static ref` form. The README shows a `lazy_static!` block containing a `static ref` of type `Regex` followed by the same `#[nutype(validate(regex = PHONE_NUMBER_REGEX))]` attribute.

Beyond regex there are `not_empty` and a `predicate` custom validator taking `&str` and returning `bool`, plus a `with` validator with a custom error type. The error variant names follow the rule mechanically: `NotEmptyViolated`, `LenCharMinViolated`, `RegexViolated`, `PredicateViolated`.

## new_unchecked is the documented escape hatch, which is the honest trade

The README's table of contents includes Breaking constraints with new_unchecked, and the examples directory has a `new_unchecked_example` and a `derive_unchecked_example` alongside `derive_unsafe` in the v0.6.2 release notes.

A guarantee you can switch off is not a guarantee, so the interesting question is how the escape hatch is shaped. Two things are visible.

First, it is named to signal intent. `new_unchecked` says in the call site that you are bypassing the invariant, which means a reviewer sees it and a grep finds it later. That is the difference between a well-designed escape hatch and a private field you unwrap in a hurry.

Second, it is its own documented section rather than an undocumented function. The README's structure implies it is meant to be used, in migration scenarios and in performance paths, and to be reviewed when used.

The v0.6.2 release is titled derive_unsafe, dated 2025-07-30. The v0.7.0 release is titled conditional derives and more, dated 2026-04-25.

The examples directory also covers constructor visibility as its own example, `const_example` for constant construction, and `custom_error` for the `with` validator path.

## A workspace where every example is a test target

The `Cargo.toml` reveals something about how this project is maintained that a single-crate repository would not show.

It is a workspace with four members, `nutype`, `nutype_macros`, `test_suite` and `dummy`, and then every example is a workspace member too. The comment explains why: all examples except `no_std_example` are tested in the test suite. The `no_std_example` line is present and commented out, which is the usual arrangement when a member needs a different build configuration than the rest.

So there are eighteen example crates included as workspace members. Reading that list tells you what the crate is actually being used for: arbitrary support, generics, `cfg_attr`, constants, custom errors, decimal percentage, unchecked derives, float sorting, integer bounding, custom sanitizer, `serde_complex`, string bounding, string regex email, string arbitrary, `no_std`, `valuable`, `&str` reference and string arbitrary.

That is a much better signal than a feature list. `integer_bounded` and `string_bounded_len` say range enforcement is a primary use. `serde_complex` says the deserialisation guarantee is tested against something more complex than a flat struct. 

There is a `GLOSSARY.md` at the root and a `_typos.toml`, which suggests the project maintains its own terminology and guards its spelling. `Cargo.lock` is committed, and there is a `Justfile` rather than a Makefile, with `art/` for the logo assets and `dummy/` as a fourth workspace member whose purpose is not described.

A proc macro crate is a build-time dependency that runs on your machine compiling your code, and the presence of `dummy/` and `.cargo/` at the root suggests the author was careful about how the macro itself is published and tested.

## Where Nutype is the wrong tool

Three boundaries, stated plainly.

The first is a genuine language constraint. Rust newtypes can be written manually. Nutype's value is generating that plus the error type plus the derive set consistently. If your invariant is not expressible as a sanitizer or validator from the available set, and cannot be written as a `predicate` or `with` closure, you have to fall back to a hand-written newtype and Nutype buys you nothing.

The second is the string limitation. Only `String` is supported, so a newtype over `&str` cannot use it as documented. That rules it out for types where borrowing is the point.

The third is scope of intent. Nutype guarantees a value is valid according to rules you wrote. It does not guarantee the rules are the right rules. A `len_char_max = 20` username validator encodes a policy about usernames; if your policy is actually about UTF-16 length because a downstream system enforces that, you have to use the UTF-16 validator, and if your policy changes the type's validation has to change with it. The macro moves the checking to the type boundary and does not remove the judgement.

On maintenance: the repository is not archived and the last push was on 2026-09-20, which is the same date as the v0.8.0 tag titled attribute passthrough, serde customization and Decimal. The release cadence is steady, roughly every four to six months across v0.6.2, v0.7.0 and v0.8.0.

The version number is worth noticing. The current release shown is v0.8.0. Reading the `CHANGELOG.md` before a major upgrade is a sensible precaution.

The licence is MIT, with a `LICENSE-MIT` file at the root.

## Conclusion

Adopt Nutype in a Rust codebase where a string or integer carries an invariant that is currently checked in scattered places, particularly if the value arrives from deserialization, since that is where hand-written validation usually leaks. Do not reach for it to add type safety to a codebase where you do not own the type definitions, since the macro generates the constructor and error type and collides with what you already have. Verify first which sanitizer and validator set your inner type supports, since the available set is determined by the inner type and string sanitizers are limited, and check whether regex validation needs both the `regex` feature and an explicit `regex` dependency.

## FAQ

### What does Nutype do exactly?

It is a proc macro that adds sanitization and validation constraints to the newtype pattern, generating a `try_new` constructor and an error enum with one variant per rule. The generated code makes it impossible to construct a value without passing the checks, including through `serde` deserialization.

### Which inner types does Nutype support?

String, which currently supports only the owned `String` type; the integer primitives from `u8` to `usize` and `isize`; the floats `f32` and `f64`; `rust_decimal::Decimal` behind the `rust_decimal` feature; and anything else. The available sanitizers, validators and derivable traits depend on the category.

### What is the difference between len_char_max and len_utf16_max?

`len_char_max` measures in chars rather than bytes, while `len_utf16_max` measures in UTF-16 code units, which the README notes is useful for JavaScript interop. A string with characters outside the basic multilingual plane can pass the char limit and exceed the UTF-16 limit.

### What do I need to use regex validation in Nutype?

Two things: the `regex` feature of `nutype` must be enabled, and you must explicitly include `regex` as a dependency. The pattern can be given in place, or as a compiled static using `std::sync::LazyLock` or `lazy_static`.

### How do I use a custom sanitizer or validator?

The `with` form takes a function or closure. As a sanitizer it receives `String` and returns `String`; as a validator it receives `&str` and returns `bool`. A `with` validator can also carry a custom error type, and the `custom_error` example shows that path.

### What can bypass Nutype's guarantees?

The README documents `new_unchecked` as a way to break constraints, and the release history includes a `derive_unsafe` feature in v0.6.2, with v0.7.0 adding conditional derives. Since a guarantee you can disable is not a guarantee, check what the derive feature relaxes before treating the type as enforced.

## Sources

- [greyblake/nutype on GitHub](https://github.com/greyblake/nutype)
- [Issues](https://github.com/greyblake/nutype/issues)
- [License: MIT](https://github.com/greyblake/nutype/blob/master/LICENSE)
- [README](https://github.com/greyblake/nutype/blob/master/README.md)
- [Releases](https://github.com/greyblake/nutype/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/greyblake-nutype
