serde_hash 0.2.0

A Rust library for seamlessly integrating HashIds with Serde serialization and deserialization. This library provides a convenient way to obfuscate numeric IDs in your JSON output without changing your application's internal data structures.
Documentation

serde_hash

A Rust library for seamlessly integrating HashIds with Serde serialization and deserialization. This library provides a convenient way to obfuscate numeric IDs in your JSON output without changing your application's internal data structures.

Features

  • Extends serde's Serialize and Deserialize derives -- all serde attributes (rename, alias, default, skip, etc.) work alongside hash encoding
  • Automatically convert numeric IDs to hash strings during serialization
  • Transparently decode hash strings back to numeric IDs during deserialization
  • Configurable hash generation with customizable salt, minimum length, and character set
  • Simple attribute-based field marking with #[serde(hash)]
  • Secure random salt generation

Installation

Add serde_hash to your Cargo.toml:

[dependencies]

serde_hash = "0.2"

serde = { version = "1.0", features = ["derive"] }

serde_json = "1.0"  # If using JSON serialization

Supported Types

The hash attribute is only compatible with unsigned integer types and collections of them. It cannot be used with floating-point (f32, f64) or signed integer (i32, i64) types.

Category Supported Types
Unsigned integers u8, u16, u32, u64, u128, usize
Optional unsigned integers Option<u8>, Option<u16>, ..., Option<usize>
Vectors of unsigned integers Vec<u8>, Vec<u16>, ..., Vec<usize>
Optional vectors Option<Vec<u8>>, Option<Vec<u16>>, ..., Option<Vec<usize>>

Usage

Configuration Options

Customize hash settings with SerdeHashOptions. Call .build() once at startup before any serialization.

Name Default Value Description
salt Generated randomly The cryptographic salt used for hash generation
min_length 8 Minimum length of the generated hash string
alphabet Alphanumeric (a-zA-Z0-9) Characters used for hash encoding

Simplest example:

use serde_hash::hashids::SerdeHashOptions;
SerdeHashOptions::new().build();

A more complete example:

use serde_hash::hashids::SerdeHashOptions;
SerdeHashOptions::new()
.with_salt("hello world")
.with_min_length(10)
.with_alphabet("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
.build();

Basic Example

Place #[serde_hash] above your derive and mark fields with #[serde(hash)]. All standard serde attributes work alongside hash:

use serde::{Serialize, Deserialize};
use serde_hash::serde_hash;
use serde_hash::hashids::SerdeHashOptions;

#[serde_hash]
#[derive(Serialize, Deserialize, Debug)]
pub struct User {
	#[serde(hash, alias = "identifier")]
	pub id: u64,
	#[serde(rename = "user_name")]
	pub name: String,
	pub age: u8,
}

fn main() {
	SerdeHashOptions::new()
		.with_salt("my-secret-salt")
		.with_min_length(10)
		.with_alphabet("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
		.build();

	let user = User {
		id: 158674,
		name: "Dan Smith".to_string(),
		age: 47,
	};

	let json_string = serde_json::to_string(&user).unwrap();
	println!("{:?} -> {}", user, json_string);
	// Output: User { id: 158674, ... } -> {"id":"qKknODM7Ej","user_name":"Dan Smith","age":47}
}

Notice how #[serde(hash, alias = "identifier")] works -- hash triggers hash encoding while alias is handled by serde normally. The rename on name also works as expected.

Deserialization Example

Hash strings are transparently decoded back to numeric IDs:

use serde::{Serialize, Deserialize};
use serde_hash::serde_hash;
use serde_hash::hashids::SerdeHashOptions;

#[serde_hash]
#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct User {
	#[serde(hash)]
	pub id: u64,
	pub name: String,
	pub age: u8,
}

fn main() {
	SerdeHashOptions::new()
		.with_salt("my-secret-salt")
		.with_min_length(10)
		.with_alphabet("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
		.build();

	let user = User { id: 158674, name: "Dan Smith".to_string(), age: 47 };
	let json = serde_json::to_string(&user).unwrap();

	let deserialized: User = serde_json::from_str(&json).unwrap();
	assert_eq!(user, deserialized);
}

Using with Vectors and Options

#[serde(hash)] works with Vec<T>, Option<T>, and Option<Vec<T>> where T is an unsigned integer:

use serde::{Serialize, Deserialize};
use serde_hash::serde_hash;
use serde_hash::hashids::SerdeHashOptions;

#[serde_hash]
#[derive(Serialize, Deserialize, Debug)]
pub struct DataWithCollections {
	#[serde(hash)]
	pub id: u64,
	pub name: String,
	#[serde(hash)]
	pub values: Vec<u8>,
	#[serde(hash)]
	pub optional_id: Option<u32>,
	#[serde(hash)]
	pub optional_values: Option<Vec<u16>>,
}

fn main() {
	SerdeHashOptions::new()
		.with_salt("my-secret-salt")
		.build();

	let data = DataWithCollections {
		id: 158674,
		name: "Dan Smith".to_string(),
		values: vec![1, 2, 3, 4, 5],
		optional_id: Some(42),
		optional_values: Some(vec![10, 20, 30]),
	};

	let json = serde_json::to_string(&data).unwrap();
	println!("{}", json);

	let deserialized: DataWithCollections = serde_json::from_str(&json).unwrap();
	println!("{:?}", deserialized);
}

Generating Secure Salt

For production use, generate a cryptographically secure random salt:

use serde_hash::{hashids::SerdeHashOptions, salt::generate_salt};

let salt = generate_salt();
SerdeHashOptions::new()
.with_salt( & salt)
.with_min_length(10)
.build();

How It Works

The #[serde_hash] attribute macro runs before serde's derive macros. It transforms #[serde(hash)] into serde's #[serde(with = "...")] attribute, pointing to built-in serialize/deserialize functions that handle hash encoding. This means:

  • Serde's own derive generates the Serialize/Deserialize implementations
  • All serde attributes (rename, alias, default, skip, flatten, etc.) work normally
  • Hash encoding is applied only to the marked fields via serde's with mechanism

Why Use serde_hash?

  • Obfuscation: Hide your internal database IDs from API consumers
  • Serde compatible: Works with all serde attributes and any serde-based format (JSON, TOML, MessagePack, etc.)
  • Predictability: Unlike UUID generation, the same ID always hashes to the same string with the same settings
  • Transparency: Your application code works with numeric IDs while your API exposes hash strings
  • Simplicity: Add one attribute macro and configure once

License

MIT License


This project is built with Rust and uses the hash-ids crate for hash ID generation.