How do I convert string cases with heck in Rust?
Walkthrough
The heck crate provides utilities for converting strings between different case styles—snake_case, camelCase, PascalCase, kebab-case, and more. It handles the complexities of word boundary detection, ensuring consistent and correct transformations. This is invaluable for code generation, configuration file handling, API serialization, and any situation where naming conventions need to be normalized or transformed.
Key concepts:
- Case styles — snake_case, camelCase, PascalCase, kebab-case, SHOUTY_SNAKE, Title Case
- Trait-based API — call
.to_snake_case(),.to_camel_case(), etc. on any string - Iterator API — iterate over words for custom transformations
- Unicode support — handles non-ASCII characters properly
- No dependencies — lightweight and fast
Code Example
# Cargo.toml
[dependencies]
heck = "0.5"use heck::ToSnakeCase;
fn main() {
let input = "helloWorld";
let output = input.to_snake_case();
println!("{}", output); // "hello_world"
}Basic Case Conversions
use heck::{
ToSnakeCase, ToCamelCase, ToPascalCase,
ToKebabCase, ToShoutySnakeCase, ToTitleCase,
};
fn main() {
let input = "hello_world";
// Snake case: hello_world
println!("snake_case: {}", input.to_snake_case());
// Camel case: helloWorld
println!("camelCase: {}", input.to_camel_case());
// Pascal case: HelloWorld
println!("PascalCase: {}", input.to_pascal_case());
// Kebab case: hello-world
println!("kebab-case: {}", input.to_kebab_case());
// Shouty snake: HELLO_WORLD
println!("SHOUTY_SNAKE: {}", input.to_shouty_snake_case());
// Title case: Hello World
println!("Title Case: {}", input.to_title_case());
}Converting Between All Cases
use heck::{
ToSnakeCase, ToCamelCase, ToPascalCase,
ToKebabCase, ToShoutySnakeCase, ToShoutyKebabCase,
ToTitleCase, ToLowerCamelCase, ToUpperCamelCase,
};
fn demonstrate_conversions(input: &str) {
println!("\nInput: '{}'", input);
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());
}
fn main() {
demonstrate_conversions("helloWorld");
demonstrate_conversions("hello_world");
demonstrate_conversions("HelloWorld");
demonstrate_conversions("hello-world");
demonstrate_conversions("HELLO_WORLD");
demonstrate_conversions("Hello World");
}Snake Case Details
use heck::ToSnakeCase;
fn main() {
// Various inputs to snake_case
let examples = [
"helloWorld", // camelCase
"HelloWorld", // PascalCase
"hello-world", // kebab-case
"HELLO_WORLD", // SHOUTY_SNAKE
"Hello World", // Title Case
"helloWorld123", // with numbers
"XMLHttpRequest", // acronyms
"userID", // ending ID
"someHTTPServer", // embedded acronym
];
for example in examples {
println!("{:20} -> {}", example, example.to_snake_case());
}
}Camel Case and Pascal Case
use heck::{ToCamelCase, ToPascalCase, ToLowerCamelCase, ToUpperCamelCase};
fn main() {
let inputs = [
"hello_world",
"user_id",
"xml_http_request",
"api_key",
"some_value_here",
];
println!("Input -> camelCase / PascalCase");
println!("================================");
for input in inputs {
println!("{} -> {} / {}",
input,
input.to_camel_case(),
input.to_pascal_case()
);
}
// to_lower_camel_case is same as to_camel_case
// to_upper_camel_case is same as to_pascal_case
println!("\nAliases:");
println!("lower_camel: {}", "hello_world".to_lower_camel_case());
println!("upper_camel: {}", "hello_world".to_upper_camel_case());
}Kebab Case
use heck::{ToKebabCase, ToShoutyKebabCase};
fn main() {
let inputs = [
"helloWorld",
"HelloWorld",
"hello_world",
"HELLO_WORLD",
"SomeLongName",
];
println!("Input -> kebab-case / SHOUTY-KEBAB");
println!("====================================");
for input in inputs {
println!("{} -> {} / {}",
input,
input.to_kebab_case(),
input.to_shouty_kebab_case()
);
}
// Common use: URL slugs
let titles = [
"My First Blog Post",
"Rust Programming Tips",
"How to Use heck",
];
println!("\nURL Slugs:");
for title in titles {
println!(" {} -> {}", title, title.to_kebab_case());
}
}Title Case
use heck::ToTitleCase;
fn main() {
let inputs = [
"hello_world",
"helloWorld",
"HELLO_WORLD",
"some-mixed-case",
"xmlHttpRequest",
];
for input in inputs {
println!("{} -> {}", input, input.to_title_case());
}
}Train Case (Mixed Case)
use heck::ToTrainCase;
fn main() {
let inputs = [
"hello_world",
"helloWorld",
"HelloWorld",
"some-mixed-case",
];
for input in inputs {
println!("{} -> {}", input, input.to_train_case());
}
// Output: Hello-World, Hello-World, Hello-World, Some-Mixed-Case
}Working with Enums
use heck::ToKebabCase;
#[derive(Debug)]
enum HttpMethod {
Get,
Post,
Put,
Delete,
Patch,
Head,
Options,
}
impl HttpMethod {
fn to_kebab(&self) -> String {
let debug = format!("{:?}", self);
debug.to_kebab_case()
}
}
fn main() {
for method in [
HttpMethod::Get,
HttpMethod::Post,
HttpMethod::Put,
HttpMethod::Delete,
HttpMethod::Patch,
] {
println!("{:?} -> {}", method, method.to_kebab());
}
}Code Generation Example
use heck::{ToSnakeCase, ToPascalCase};
struct Field {
name: String,
type_name: String,
}
struct Struct {
name: String,
fields: Vec<Field>,
}
fn generate_rust_struct(s: &Struct) -> String {
let struct_name = s.name.to_pascal_case();
let fields: String = s.fields
.iter()
.map(|f| {
let field_name = f.name.to_snake_case();
format!(" {}: {},", field_name, f.type_name)
})
.collect::<Vec<_>>()
.join("\n");
format!("struct {} {{\n{}\n}}", struct_name, fields)
}
fn generate_builder_methods(s: &Struct) -> String {
let struct_name = s.name.to_pascal_case();
s.fields
.iter()
.map(|f| {
let field_name = f.name.to_snake_case();
let method_name = field_name.clone();
format!(
r#" pub fn {}(mut self, value: {}) -> Self {{
self.{} = value;
self
}}"#,
method_name, f.type_name, field_name
)
})
.collect::<Vec<_>>()
.join("\n\n")
}
fn main() {
let user_struct = Struct {
name: "user_profile".to_string(),
fields: vec![
Field { name: "firstName".to_string(), type_name: "String".to_string() },
Field { name: "lastName".to_string(), type_name: "String".to_string() },
Field { name: "emailAddress".to_string(), type_name: "String".to_string() },
Field { name: "isActive".to_string(), type_name: "bool".to_string() },
],
};
println!("{}\n", generate_rust_struct(&user_struct));
println!("// Builder methods:\n{}", generate_builder_methods(&user_struct));
}Database Column Mapping
use heck::ToSnakeCase;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct User {
#[serde(rename = "user_id")]
user_id: u32,
#[serde(rename = "first_name")]
first_name: String,
#[serde(rename = "last_name")]
last_name: String,
#[serde(rename = "created_at")]
created_at: String,
}
// Helper function to generate column names
fn to_column_name(field: &str) -> String {
field.to_snake_case()
}
fn main() {
let fields = ["userId", "firstName", "lastName", "createdAt", "isActive"];
println!("Field -> Database Column");
println!("========================");
for field in fields {
println!("{:15} -> {}", field, to_column_name(field));
}
}CLI Argument Conversion
use heck::{ToKebabCase, ToSnakeCase};
// Convert struct field names to CLI argument names
fn field_to_cli_arg(field: &str) -> String {
format!("--{}", field.to_kebab_case())
}
// Convert CLI arg back to struct field
fn cli_arg_to_field(arg: &str) -> String {
arg.trim_start_matches("--")
.to_snake_case()
}
fn main() {
let field_names = [
"outputFile",
"inputFormat",
"maxRetries",
"enableVerbose",
"configPath",
];
println!("Field -> CLI Argument -> Field");
println!("================================");
for field in field_names {
let cli_arg = field_to_cli_arg(field);
let back = cli_arg_to_field(&cli_arg);
println!("{:15} -> {:20} -> {}", field, cli_arg, back);
}
}URL Slug Generation
use heck::ToKebabCase;
fn generate_slug(title: &str) -> String {
title.to_kebab_case()
}
fn main() {
let blog_titles = [
"Hello World!",
"Rust Programming: A Deep Dive",
"How to Install Rust on macOS",
"Understanding Ownership in Rust",
"WebAssembly with Rust in 2024",
];
println!("Blog Title -> URL Slug");
println!("=======================");
for title in blog_titles {
println!("{} -> {}", title, generate_slug(title));
}
}API Response Normalization
use heck::{ToSnakeCase, ToCamelCase};
use serde_json::{json, Value};
// Convert JSON keys to snake_case (database format)
fn to_db_format(json: &Value) -> Value {
match json {
Value::Object(map) => {
let new_map: serde_json::Map<String, Value> = map
.into_iter()
.map(|(k, v)| (k.to_snake_case(), to_db_format(v)))
.collect();
Value::Object(new_map)
}
Value::Array(arr) => {
Value::Array(arr.iter().map(to_db_format).collect())
}
other => other.clone(),
}
}
// Convert JSON keys to camelCase (API format)
fn to_api_format(json: &Value) -> Value {
match json {\n Value::Object(map) => {
let new_map: serde_json::Map<String, Value> = map
.into_iter()
.map(|(k, v)| (k.to_camel_case(), to_api_format(v)))
.collect();
Value::Object(new_map)
}
Value::Array(arr) => {
Value::Array(arr.iter().map(to_api_format).collect())
}
other => other.clone(),
}
}
fn main() {
let api_response = json!({
"userId": 123,
"firstName": "John",
"lastName": "Doe",
"emailAddress": "john@example.com",
"profileSettings": {
"enableNotifications": true,
"preferredLanguage": "en"
}
});
println!("API Format (camelCase):");
println!("{}\n", serde_json::to_string_pretty(&api_response).unwrap());
let db_format = to_db_format(&api_response);
println!("DB Format (snake_case):");
println!("{}", serde_json::to_string_pretty(&db_format).unwrap());
}Word Iterator for Custom Transformations
use heck::StrIteratorExt;
fn main() {
let input = "hello_world_example";
// Iterate over words in the string
println!("Words in '{}':", input);
for word in input.split_word_bounds() {
println!(" {:?}", word);
}
// Custom transformation: UPPERCASE.WITH.DOTS
let custom: String = input
.split_word_bounds()
.map(|w| w.to_uppercase())
.collect::<Vec<_>>()
.join(".");
println!("\nCustom format: {}", custom);
// Custom: First letter of each word
let initials: String = input
.split_word_bounds()
.filter_map(|w| w.chars().next())
.collect();
println!("Initials: {}", initials);
}Mixed Input Handling
use heck::ToSnakeCase;
fn main() {
// Heck handles various input formats intelligently
let mixed_inputs = [
"AlreadySnakeCase",
"camelCaseInput",
"kebab-case-input",
"SHOUTY_SNAKE_INPUT",
"Title Case Input",
"mixed_Case-Input",
"JSON2XMLConverter",
"XMLHttpRequest",
"parseURL",
"getUserID",
];
println!("Mixed Input -> snake_case");
println!("==========================");
for input in mixed_inputs {
println!("{:25} -> {}", input, input.to_snake_case());
}
}Config File Key Conversion
use heck::{ToKebabCase, ToSnakeCase};
struct ConfigKey {
name: String,
default: String,
description: String,
}
impl ConfigKey {
fn env_var(&self) -> String {
self.name.to_shouty_snake_case()
}
fn config_key(&self) -> String {
self.name.to_kebab_case()
}
fn field_name(&self) -> String {
self.name.to_snake_case()
}
}
trait ToShoutySnakeCase {
fn to_shouty_snake_case(&self) -> String;
}
impl ToShoutySnakeCase for str {
fn to_shouty_snake_case(&self) -> String {
self.to_snake_case().to_uppercase()
}
}
fn main() {
let configs = [
ConfigKey {
name: "serverPort".to_string(),
default: "8080".to_string(),
description: "Port for the server".to_string(),
},
ConfigKey {
name: "databaseUrl".to_string(),
default: "localhost".to_string(),
description: "Database connection URL".to_string(),
},
ConfigKey {
name: "maxConnections".to_string(),
default: "10".to_string(),
description: "Maximum database connections".to_string(),
},
];
println!("Config Field | Env Variable | Config Key");
println!("-------------|-----------------|----------------");
for config in &configs {
println!(
"{:12} | {:15} | {}",
config.field_name(),
config.env_var(),
config.config_key()
);
}
}Trait-based Implementation
use heck::{ToSnakeCase, ToCamelCase};
trait CaseConvertible: AsRef<str> {
fn as_snake(&self) -> String {
self.as_ref().to_snake_case()
}
fn as_camel(&self) -> String {
self.as_ref().to_camel_case()
}
fn as_pascal(&self) -> String {
self.as_ref().to_pascal_case()
}
fn as_kebab(&self) -> String {
self.as_ref().to_kebab_case()
}
}
impl CaseConvertible for str {}
impl CaseConvertible for String {}
fn process_name<T: CaseConvertible>(name: &T) {
println!("Original: {}", name.as_ref());
println!(" snake: {}", name.as_snake());
println!(" camel: {}", name.as_camel());
println!(" pascal: {}", name.as_pascal());
println!(" kebab: {}", name.as_kebab());
}
fn main() {
process_name(&"helloWorld");
println!();
process_name(&String::from("some_value"));
}Real-World: OpenAPI Schema Generator
use heck::{ToPascalCase, ToSnakeCase, ToCamelCase};
struct ApiEndpoint {
path: String,
method: String,
operation_id: String,
}
impl ApiEndpoint {
fn handler_name(&self) -> String {
self.operation_id.to_snake_case()
}
fn struct_name(&self) -> String {
self.operation_id.to_pascal_case()
}
fn route_name(&self) -> String {
self.path
.trim_start_matches('/')
.replace("/", "_")
.replace("{", "")
.replace("}", "")
.to_snake_case()
}
}
fn main() {
let endpoints = [
ApiEndpoint {
path: "/users".to_string(),
method: "GET".to_string(),
operation_id: "listUsers".to_string(),
},
ApiEndpoint {
path: "/users/{id}".to_string(),
method: "GET".to_string(),
operation_id: "getUserById".to_string(),
},
ApiEndpoint {
path: "/users/{userId}/posts".to_string(),
method: "POST".to_string(),
operation_id: "createUserPost".to_string(),
},
];
println!("Operation ID -> Handler / Struct / Route");
println!("===========================================");
for endpoint in &endpoints {
println!(
"{:15} -> {} / {} / {}",
endpoint.operation_id,
endpoint.handler_name(),
endpoint.struct_name(),
endpoint.route_name()
);
}
}Performance Considerations
use heck::ToSnakeCase;
fn main() {
// All allocations happen in the output String
// Input string is not modified
let input = "helloWorld";
let output = input.to_snake_case();
println!("Original: {}", input); // Still "helloWorld"
println!("Converted: {}", output); // "hello_world"
// For repeated conversions, consider caching
let cached_names: Vec<(String, String)> = [
"userId", "firstName", "lastName", "createdAt"
]
.iter()
.map(|&s| (s.to_string(), s.to_snake_case()))
.collect();
for (original, snake) in &cached_names {
println!("{} -> {}", original, snake);
}
}Summary
to_snake_case()converts tohello_worldformatto_camel_case()converts tohelloWorldformat (lowerCamel)to_pascal_case()/to_upper_camel_case()converts toHelloWorldformatto_kebab_case()converts tohello-worldformatto_shouty_snake_case()converts toHELLO_WORLDformatto_title_case()converts toHello Worldformat- Works on
&str,String, and any type implementingAsRef<str> - Handles mixed input formats intelligently
- No external dependencies, fast and lightweight
- Perfect for: code generation, API serialization, CLI tools, URL slugs, config files
