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:

  1. Case styles — snake_case, camelCase, PascalCase, kebab-case, SHOUTY_SNAKE, Title Case
  2. Trait-based API — call .to_snake_case(), .to_camel_case(), etc. on any string
  3. Iterator API — iterate over words for custom transformations
  4. Unicode support — handles non-ASCII characters properly
  5. 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 to hello_world format
  • to_camel_case() converts to helloWorld format (lowerCamel)
  • to_pascal_case() / to_upper_camel_case() converts to HelloWorld format
  • to_kebab_case() converts to hello-world format
  • to_shouty_snake_case() converts to HELLO_WORLD format
  • to_title_case() converts to Hello World format
  • Works on &str, String, and any type implementing AsRef<str>
  • Handles mixed input formats intelligently
  • No external dependencies, fast and lightweight
  • Perfect for: code generation, API serialization, CLI tools, URL slugs, config files