How do I convert string case with heck in Rust?
Walkthrough
The heck crate provides case conversion utilities for transforming strings between different naming conventions. It supports converting to snake_case, camelCase, PascalCase, kebab-case, SCREAMING_SNAKE_CASE, and more. The crate handles edge cases like acronyms, numbers, and special characters gracefully. It's particularly useful for code generators, serialization, CLI tools, and any situation where you need to transform identifiers between conventions (like converting database column names to Rust struct fields).
Key concepts:
- Snake case —
snake_casewith underscores, lowercase - Camel case —
camelCasewith leading lowercase, no separators - Pascal case —
PascalCaseorUpperCamelCase, each word capitalized - Kebab case —
kebab-casewith hyphens, lowercase - Screaming snake —
SCREAMING_SNAKE_CASE, all caps with underscores
Code Example
# Cargo.toml
[dependencies]
heck = "0.5"use heck::{ToSnakeCase, ToCamelCase, ToPascalCase, ToKebabCase, ToShoutySnakeCase};
fn main() {
let input = "hello world";
println!("Snake: {}", input.to_snake_case());
println!("Camel: {}", input.to_camel_case());
println!("Pascal: {}", input.to_pascal_case());
println!("Kebab: {}", input.to_kebab_case());
println!("Shouty: {}", input.to_shouty_snake_case());
}Basic Case Conversions
use heck::{
ToSnakeCase, ToCamelCase, ToPascalCase,
ToKebabCase, ToShoutySnakeCase, ToShoutyKebabCase,
ToTitleCase, ToLowerCamelCase,
};
fn main() {
let input = "hello world example";
println!("Input: {}", input);
println!("");
println!("snake_case: {}", input.to_snake_case());
println!("camelCase: {}", input.to_camel_case());
println!("PascalCase: {}", input.to_pascal_case());
println!("kebab-case: {}", input.to_kebab_case());
println!("SHOUTY_SNAKE: {}", input.to_shouty_snake_case());
println!("SHOUTY-KEBAB: {}", input.to_shouty_kebab_case());
println!("Title Case: {}", input.to_title_case());
println!("lowerCamelCase: {}", input.to_lower_camel_case());
}Converting from Different Formats
use heck::{
ToSnakeCase, ToCamelCase, ToPascalCase,
ToKebabCase, ToShoutySnakeCase,
};
fn main() {
// From snake_case
let snake = "user_account_settings";
println!("From snake_case: {}", snake);
println!(" camel: {}", snake.to_camel_case());
println!(" pascal: {}", snake.to_pascal_case());
println!(" kebab: {}", snake.to_kebab_case());
// From camelCase
let camel = "userAccountSettings";
println!("\nFrom camelCase: {}", camel);
println!(" snake: {}", camel.to_snake_case());
println!(" pascal: {}", camel.to_pascal_case());
println!(" kebab: {}", camel.to_kebab_case());
// From PascalCase
let pascal = "UserAccountSettings";
println!("\nFrom PascalCase: {}", pascal);
println!(" snake: {}", pascal.to_snake_case());
println!(" camel: {}", pascal.to_camel_case());
println!(" kebab: {}", pascal.to_kebab_case());
// From kebab-case
let kebab = "user-account-settings";
println!("\nFrom kebab-case: {}", kebab);
println!(" snake: {}", kebab.to_snake_case());
println!(" camel: {}", kebab.to_camel_case());
println!(" pascal: {}", kebab.to_pascal_case());
}Handling Numbers
use heck::{ToSnakeCase, ToCamelCase, ToPascalCase, ToKebabCase};
fn main() {
let inputs = vec![
"user2fa",
"oauth2client",
"version2",
"html5",
"http2",
"ipv4",
"ipv6",
"sha256",
"aes128",
];
println!("Handling numbers:");
for input in inputs {
println!("\nInput: {}", input);
println!(" snake: {}", input.to_snake_case());
println!(" camel: {}", input.to_camel_case());
println!(" pascal: {}", input.to_pascal_case());
println!(" kebab: {}", input.to_kebab_case());
}
}Handling Acronyms
use heck::{ToSnakeCase, ToCamelCase, ToPascalCase, ToKebabCase};
fn main() {
let inputs = vec![
"XMLParser",
"HTTPServer",
"HTTPSConnection",
"APIResponse",
"IOError",
"URLBuilder",
"HTMLDocument",
"JSONSerializer",
"SQLDatabase",
"UUIDGenerator",
];
println!("Acronyms:");
for input in inputs {
println!("\nInput: {}", input);
println!(" snake: {}", input.to_snake_case());
println!(" camel: {}", input.to_camel_case());
println!(" pascal: {}", input.to_pascal_case());
println!(" kebab: {}", input.to_kebab_case());
}
}Mixed Input Formats
use heck::{ToSnakeCase, ToCamelCase, ToPascalCase, ToKebabCase};
fn main() {
let inputs = vec![
"helloWorld",
"hello_world",
"hello-world",
"HelloWorld",
"HELLO_WORLD",
"helloWorld123",
"Hello_World-Test",
"already_snake_case",
"MixedCaseAnd_snake-mix",
];
println!("Mixed inputs:");
println!("{:<25} {:<20} {:<20} {:<20}", "Input", "snake", "camel", "pascal");
println!("{}", "-".repeat(85));
for input in inputs {
println!(
"{:<25} {:<20} {:<20} {:<20}",
input,
input.to_snake_case(),
input.to_camel_case(),
input.to_pascal_case()
);
}
}Title Case
use heck::ToTitleCase;
fn main() {
let inputs = vec![
"hello_world",
"camelCase",
"PascalCase",
"kebab-case",
"SCREAMING_SNAKE",
"mixed_caseExample",
];
println!("Title Case conversions:");
for input in inputs {
println!(" {} -> {}", input, input.to_title_case());
}
}Train Case (Upper Kebab)
use heck::ToTrainCase;
fn main() {
let inputs = vec![
"hello_world",
"camelCase",
"PascalCase",
"SCREAMING_SNAKE",
];
println!("Train Case (Upper-Kebab):");
for input in inputs {
println!(" {} -> {}", input, input.to_train_case());
}
}Cobol Case (Upper Snake)
use heck::ToUpperCamelCase;
fn main() {
let inputs = vec![
"hello_world",
"camelCase",
"kebab-case",
"SCREAMING_SNAKE",
];
println!("Upper Camel (Pascal/Cobol):");
for input in inputs {
println!(" {} -> {}", input, input.to_upper_camel_case());
}
}Real-World: Database to Rust Struct
use heck::ToPascalCase;
struct DatabaseColumn {
name: String,
rust_name: String,
}
impl DatabaseColumn {
fn from_db(name: &str) -> Self {
Self {
name: name.to_string(),
rust_name: name.to_pascal_case(),
}
}
}
fn generate_struct(name: &str, columns: &[&str]) -> String {
let struct_name = name.to_pascal_case();
let fields: Vec<String> = columns
.iter()
.map(|col| {
let rust_name = col.to_snake_case();
format!(" pub {}: String,", rust_name)
})
.collect();
format!(
"pub struct {} {{\n{}\n}}",
struct_name,
fields.join("\n")
)
}
fn main() {
let columns = vec![
"user_id",
"first_name",
"last_name",
"email_address",
"created_at",
"updated_at",
"is_active",
];
let rust_struct = generate_struct("user_account", &columns);
println!("{}", rust_struct);
}Real-World: API Response Transformer
use heck::{ToSnakeCase, ToCamelCase};
use std::collections::HashMap;
fn transform_keys_to_snake(map: HashMap<String, String>) -> HashMap<String, String> {
map.into_iter()
.map(|(k, v)| (k.to_snake_case(), v))
.collect()
}
fn transform_keys_to_camel(map: HashMap<String, String>) -> HashMap<String, String> {
map.into_iter()
.map(|(k, v)| (k.to_camel_case(), v))
.collect()
}
fn main() {
// Simulate API response with camelCase keys
let api_response: HashMap<String, String> = vec![
("userId".to_string(), "123".to_string()),
("firstName".to_string(), "John".to_string()),
("lastName".to_string(), "Doe".to_string()),
("emailAddress".to_string(), "john@example.com".to_string()),
("createdAt".to_string(), "2024-01-01".to_string()),
("isActive".to_string(), "true".to_string()),
]
.into_iter()
.collect();
println!("Original API response (camelCase):");
for (key, value) in &api_response {
println!(" {}: {}", key, value);
}
// Convert to snake_case for internal use
let snake_case = transform_keys_to_snake(api_response.clone());
println!("\nConverted to snake_case:");
for (key, value) in &snake_case {
println!(" {}: {}", key, value);
}
}Real-World: CLI Flag Generator
use heck::{ToKebabCase, ToSnakeCase};
struct CliFlag {
name: String,
short: char,
long: String,
env_var: String,
help: String,
}
impl CliFlag {
fn new(name: &str, help: &str) -> Self {
let kebab = name.to_kebab_case();
let short = kebab.chars().next().unwrap();
Self {
name: name.to_snake_case(),
short,
long: kebab,
env_var: name.to_shouty_snake_case(),
help: help.to_string(),
}
}
fn generate_clap_code(&self) -> String {
format!(
r#".arg(
Arg::new("{}")
.short('{}')
.long("{}")
.env("{}")
.help("{}")
)"#,
self.name,
self.short,
self.long,
self.env_var,
self.help
)
}
}
fn main() {
let flags = vec![
CliFlag::new("output_file", "Output file path"),
CliFlag::new("max_retries", "Maximum number of retries"),
CliFlag::new("api_key", "API authentication key"),
CliFlag::new("enable_ssl", "Enable SSL encryption"),
CliFlag::new("database_url", "Database connection URL"),
];
println!("Generated CLI flags:\n");
for flag in flags {
println!("{}\n", flag.generate_clap_code());
}
}
// Need to import ToShoutySnakeCase
use heck::ToShoutySnakeCase;Real-World: Enum Variant Generator
use heck::{ToPascalCase, ToSnakeCase, ToShoutySnakeCase};
struct EnumVariant {
original: String,
pascal: String,
snake: String,
shouty: String,
}
impl EnumVariant {
fn from(input: &str) -> Self {
Self {
original: input.to_string(),
pascal: input.to_pascal_case(),
snake: input.to_snake_case(),
shouty: input.to_shouty_snake_case(),
}
}
}
fn generate_enum(name: &str, values: &[&str]) -> String {
let enum_name = name.to_pascal_case();
let variants: Vec<String> = values
.iter()
.map(|v| format!(" {},", v.to_pascal_case()))
.collect();
let match_arms: Vec<String> = values
.iter()
.map(|v| {
let pascal = v.to_pascal_case();
let snake = v.to_snake_case();
format!(" \"{}\" => {}::{},", snake, enum_name, pascal)
})
.collect();
format!(
r#"pub enum {} {{
{}}}
impl {} {{
pub fn from_str(s: &str) -> Option<Self> {{
match s {{
{}
_ => None,
}}
}}
}}"#,
enum_name,
variants.join("\n"),
enum_name,
match_arms.join("\n")
)
}
fn main() {
let statuses = vec!["pending", "in_progress", "completed", "failed", "cancelled"];
let enum_code = generate_enum("status", &statuses);
println!("{}", enum_code);
}Real-World: SQL Column Name Converter
use heck::{ToSnakeCase, ToPascalCase};
struct ColumnMapping {
db_column: String,
rust_field: String,
getter: String,
setter: String,
}
impl ColumnMapping {
fn from_db_name(db_name: &str) -> Self {
let snake = db_name.to_snake_case();
let pascal = db_name.to_pascal_case();
Self {
db_column: db_name.to_string(),
rust_field: snake,
getter: format!("get_{}", db_name.to_snake_case()),
setter: format!("set_{}", db_name.to_snake_case()),
}
}
fn generate_accessors(&self, type_name: &str) -> String {
format!(
r#" pub fn {}(&self) -> &{} {{
&self.{}
}}
pub fn {}(&mut self, value: {}) {{
self.{} = value;
}}"#,
self.getter,
type_name,
self.rust_field,
self.setter,
type_name,
self.rust_field
)
}
}
fn main() {
let columns = vec![
ColumnMapping::from_db_name("user_id"),
ColumnMapping::from_db_name("firstName"),
ColumnMapping::from_db_name("EMAIL_ADDRESS"),
ColumnMapping::from_db_name("created-at"),
];
for col in columns {
println!("DB: {:<15} Rust: {:<15} Getter: {:<20} Setter: {}",
col.db_column,
col.rust_field,
col.getter,
col.setter);
}
}Real-World: JSON Key Transformer
use heck::{ToSnakeCase, ToCamelCase, ToPascalCase};
use serde_json::{json, Value};
fn transform_json_keys(value: Value, to_case: fn(&str) -> String) -> Value {
match value {
Value::Object(map) => {
let new_map = map
.into_iter()
.map(|(k, v)| (to_case(&k), transform_json_keys(v, to_case)))
.collect();
Value::Object(new_map)
}
Value::Array(arr) => {
Value::Array(arr.into_iter().map(|v| transform_json_keys(v, to_case)).collect())
}
other => other,
}
}
fn main() {
let data = json!({
"userName": "john_doe",
"userId": 123,
"emailAddress": "john@example.com",
"accountSettings": {
"enableNotifications": true,
"preferredLanguage": "en"
},
"orderHistory": [
{"orderId": 1, "orderDate": "2024-01-01"},
{"orderId": 2, "orderDate": "2024-01-15"}
]
});
println!("Original (camelCase):");
println!("{}\n", serde_json::to_string_pretty(&data).unwrap());
println!("Converted to snake_case:");
let snake = transform_json_keys(data.clone(), |s| s.to_snake_case());
println!("{}\n", serde_json::to_string_pretty(&snake).unwrap());
}Real-World: URL Slug Generator
use heck::ToKebabCase;
fn generate_slug(title: &str) -> String {
title.to_kebab_case()
}
fn generate_unique_slug(title: &str, existing: &[String]) -> String {
let base_slug = generate_slug(title);
if !existing.contains(&base_slug) {
return base_slug;
}
let mut counter = 1;
loop {
let slug = format!("{}-{}", base_slug, counter);
if !existing.contains(&slug) {
return slug;
}
counter += 1;
}
}
fn main() {
let titles = vec![
"Hello World",
"My First Blog Post",
"Rust Programming Language",
"Hello World", // Duplicate
"10 Tips for Better Code",
"API Design Best Practices",
];
let mut existing: Vec<String> = Vec::new();
for title in titles {
let slug = generate_unique_slug(title, &existing);
existing.push(slug.clone());
println!("'{}' -> '{}'", title, slug);
}
}Real-World: Environment Variable Names
use heck::ToShoutySnakeCase;
struct ConfigField {
name: String,
env_var: String,
default: Option<String>,
}
impl ConfigField {
fn new(name: &str, default: Option<&str>) -> Self {
Self {
name: name.to_string(),
env_var: name.to_shouty_snake_case(),
default: default.map(|s| s.to_string()),
}
}
fn generate_config_code(&self) -> String {
let default_code = match &self.default {
Some(val) => format!(", default = \"{}\"", val),
None => String::new(),
};
format!(
" #[serde(rename = \"{}\"){}]\n pub {}: String,",
self.name,
default_code,
self.name.to_snake_case()
)
}
fn generate_env_var(&self) -> String {
format!("export {}=\"\"", self.env_var)
}
}
use heck::ToSnakeCase;
fn main() {
let config_fields = vec![
ConfigField::new("database_url", Some("localhost:5432")),
ConfigField::new("api_key", None),
ConfigField::new("max_connections", Some("10")),
ConfigField::new("enable_ssl", Some("true")),
ConfigField::new("log_level", Some("info")),
];
println!("Config struct fields:\n");
for field in &config_fields {
println!("{}", field.generate_config_code());
}
println!("\n\nEnvironment variables:\n");
for field in &config_fields {
println!("{}", field.generate_env_var());
}
}Real-World: Method Name Generator
use heck::{ToSnakeCase, ToCamelCase};
struct Method {
name: String,
snake: String,
camel: String,
params: Vec<(String, String)>,
return_type: String,
}
impl Method {
fn new(name: &str, params: Vec<(&str, &str)>, return_type: &str) -> Self {
Self {
name: name.to_string(),
snake: name.to_snake_case(),
camel: name.to_camel_case(),
params: params
.iter()
.map(|(n, t)| (n.to_snake_case(), t.to_string()))
.collect(),
return_type: return_type.to_string(),
}
}
fn generate_rust(&self) -> String {
let params: String = self.params
.iter()
.map(|(n, t)| format!("{}: {}", n, t))
.collect::<Vec<_>>()
.join(", ");
format!(
"fn {}({}) -> {} {{\n // TODO: implement\n }}",
self.snake, params, self.return_type
)
}
fn generate_typescript(&self) -> String {
let params: String = self.params
.iter()
.map(|(n, t)| format!("{}: {}", n.to_camel_case(), t))
.collect::<Vec<_>>()
.join(", ");
format!(
"{}({}): {} {{\n // TODO: implement\n}}",
self.camel, params, self.return_type
)
}
}
fn main() {
let methods = vec![
Method::new("get_user_by_id", vec![("user_id", "i32")], "Option<User>"),
Method::new("create_new_user", vec![("user_data", "UserData")], "Result<User>"),
Method::new("update_user_settings", vec![("user_id", "i32"), ("settings", "Settings")], "Result<()>"),
Method::new("delete_user_account", vec![("user_id", "i32")], "Result<bool>"),
];
println!("Rust methods:\n");
for method in &methods {
println!("{}\n", method.generate_rust());
}
println!("\nTypeScript methods:\n");
for method in &methods {
println!("{}\n", method.generate_typescript());
}
}Mixed Case Detection
use heck::{ToSnakeCase, ToCamelCase, ToPascalCase, ToKebabCase};
fn detect_case(input: &str) -> &'static str {
let has_underscore = input.contains('_');
let has_hyphen = input.contains('-');
let has_upper = input.chars().any(|c| c.is_uppercase());
let has_lower = input.chars().any(|c| c.is_lowercase());
let all_upper = input.chars().filter(|c| c.is_alphabetic()).all(|c| c.is_uppercase());
let first_upper = input.chars().next().map(|c| c.is_uppercase()).unwrap_or(false);
if has_underscore && all_upper {
"SCREAMING_SNAKE_CASE"
} else if has_underscore && has_lower {
"snake_case"
} else if has_hyphen {
"kebab-case"
} else if first_upper && has_upper {
"PascalCase"
} else if has_upper {
"camelCase"
} else {
"unknown"
}
}
fn normalize(input: &str, target: &str) -> String {
match target {
"snake" => input.to_snake_case(),
"camel" => input.to_camel_case(),
"pascal" => input.to_pascal_case(),
"kebab" => input.to_kebab_case(),
_ => input.to_string(),
}
}
fn main() {
let inputs = vec![
"user_name",
"userName",
"UserName",
"user-name",
"USER_NAME",
];
println!("Case detection and normalization:\n");
println!("{:<15} {:<20} {:<15} {:<15} {:<15}",
"Input", "Detected", "snake", "camel", "pascal");
println!("{}", "-".repeat(80));
for input in inputs {
println!(
"{:<15} {:<20} {:<15} {:<15} {:<15}",
input,
detect_case(input),
normalize(input, "snake"),
normalize(input, "camel"),
normalize(input, "pascal")
);
}
}Batch Case Converter
use heck::{
ToSnakeCase, ToCamelCase, ToPascalCase,
ToKebabCase, ToShoutySnakeCase, ToTitleCase,
};
struct CaseConverter {
input: String,
}
impl CaseConverter {
fn new(input: &str) -> Self {
Self { input: input.to_string() }
}
fn to_all_cases(&self) -> CaseOutput {
CaseOutput {
original: self.input.clone(),
snake: self.input.to_snake_case(),
camel: self.input.to_camel_case(),
pascal: self.input.to_pascal_case(),
kebab: self.input.to_kebab_case(),
shouty: self.input.to_shouty_snake_case(),
title: self.input.to_title_case(),
}
}
}
struct CaseOutput {
original: String,
snake: String,
camel: String,
pascal: String,
kebab: String,
shouty: String,
title: String,
}
impl CaseOutput {
fn print_table(&self) {
println!("Original: {}", self.original);
println!("snake_case: {}", self.snake);
println!("camelCase: {}", self.camel);
println!("PascalCase: {}", self.pascal);
println!("kebab-case: {}", self.kebab);
println!("SHOUTY_SNAKE: {}", self.shouty);
println!("Title Case: {}", self.title);
}
}
fn main() {
let inputs = vec![
"helloWorld",
"user_account_settings",
"APIResponseHandler",
"some-mixed-INPUT",
];
for input in inputs {
println!("\n{}", "=".repeat(50));
let converter = CaseConverter::new(input);
let output = converter.to_all_cases();
output.print_table();
}
}Summary
to_snake_case()— converts tolower_case_with_underscoresto_camel_case()— converts tolowerCamelCaseto_pascal_case()/to_upper_camel_case()— converts toUpperCamelCaseto_kebab_case()— converts tolower-case-with-hyphensto_shouty_snake_case()— converts toUPPER_CASE_WITH_UNDERSCORESto_shouty_kebab_case()— converts toUPPER-CASE-WITH-HYPHENSto_title_case()— converts toTitle Caseto_train_case()— converts toTrain-Case- Handles mixed input formats (snake, camel, pascal, kebab)
- Handles acronyms and numbers appropriately
- Perfect for: code generators, API transformers, CLI tools, ORMs
