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:

  1. Snake casesnake_case with underscores, lowercase
  2. Camel casecamelCase with leading lowercase, no separators
  3. Pascal casePascalCase or UpperCamelCase, each word capitalized
  4. Kebab casekebab-case with hyphens, lowercase
  5. Screaming snakeSCREAMING_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 to lower_case_with_underscores
  • to_camel_case() — converts to lowerCamelCase
  • to_pascal_case() / to_upper_camel_case() — converts to UpperCamelCase
  • to_kebab_case() — converts to lower-case-with-hyphens
  • to_shouty_snake_case() — converts to UPPER_CASE_WITH_UNDERSCORES
  • to_shouty_kebab_case() — converts to UPPER-CASE-WITH-HYPHENS
  • to_title_case() — converts to Title Case
  • to_train_case() — converts to Train-Case
  • Handles mixed input formats (snake, camel, pascal, kebab)
  • Handles acronyms and numbers appropriately
  • Perfect for: code generators, API transformers, CLI tools, ORMs