Convert String to Enum in Groovy – 12 Tested Examples

every way to handle Groovy string to enum conversion with 12 examples. valueOf, safe conversion, case-insensitive lookup, and more. Complete guide for Groovy 5.x.

“Converting a string to an enum is simple until it isn’t. Handle the edge cases up front, and you’ll thank yourself every time bad data hits your system.”

Robert C. Martin, Clean Code

Last Updated: March 2026 | Tested on: Groovy 5.x, Java 17+ | Difficulty: Beginner to Intermediate | Reading Time: 16 minutes

A groovy string to enum conversion sounds simple enough – and it is, when the string matches perfectly. But real data comes from user input, JSON payloads, database columns, and config files, where you hit case differences, trailing whitespace, invalid values, and null strings. That’s where knowing all your conversion options pays off.

This post covers every way to convert a String to an Enum in Groovy. We’ll walk through 12 tested examples showing valueOf(), Groovy’s as keyword, case-insensitive lookups, safe conversion with defaults, reverse lookups by property, and patterns for handling enums in real-world applications like JSON processing and API responses.

If you’re new to enums, start with our complete Groovy Enum guide first. And if you’ve worked with other string conversions, our String to Integer guide follows similar patterns.

Every example is tested on Groovy 5.x with exact output shown. Let’s get into it.

The Basics – valueOf() and as Keyword

Groovy provides two primary ways to convert a string to an enum: Java’s valueOf() method and Groovy’s as keyword. Both require an exact case-sensitive match with the enum constant name.

Basic String to Enum Conversion

enum Color {
    RED, GREEN, BLUE, YELLOW, PURPLE
}

// Method 1: valueOf() - Java standard approach
def red = Color.valueOf("RED")
println "valueOf: ${red} (type: ${red.class.simpleName})"

// Method 2: Groovy 'as' keyword
def blue = "BLUE" as Color
println "as keyword: ${blue} (type: ${blue.class.simpleName})"

// Method 3: Enum.valueOf() with explicit class
def green = Enum.valueOf(Color, "GREEN")
println "Enum.valueOf: ${green}"

// All three produce the same result
assert Color.valueOf("YELLOW") == ("YELLOW" as Color)
assert Color.valueOf("YELLOW") == Enum.valueOf(Color, "YELLOW")
println "\nAll methods produce identical results: true"

// String must match exactly (case-sensitive)
println "\nCase sensitivity:"
println "  'RED' -> ${Color.valueOf('RED')}"
try {
    Color.valueOf("red")  // This will fail
} catch (IllegalArgumentException e) {
    println "  'red' -> ERROR: ${e.message}"
}

Output

valueOf: RED (type: Color)
as keyword: BLUE (type: Color)
Enum.valueOf: GREEN

All methods produce identical results: true

Case sensitivity:
  'RED' -> RED
  'red' -> ERROR: No enum constant Color.red

The key thing to remember: both valueOf() and as require an exact match on the constant name. They’re case-sensitive and throw IllegalArgumentException if no match is found. This is by design – enums are strict, and an unrecognized value should be treated as an error.

Why String to Enum Conversion Fails

According to the Groovy GDK documentation, the most common failure reasons are:

  • Case mismatch"red" vs "RED"
  • Whitespace" RED " has leading/trailing spaces
  • Invalid value"ORANGE" doesn’t exist in the enum
  • Null inputnull passed to valueOf()
  • Display name vs constant name – trying to use "Not Found" instead of "NOT_FOUND"

Common Failure Scenarios

enum Status { ACTIVE, INACTIVE, PENDING }

def tryConvert(String input) {
    try {
        def result = Status.valueOf(input)
        println "  '${input}' -> ${result}"
    } catch (Exception e) {
        println "  '${input}' -> FAILED: ${e.class.simpleName} - ${e.message}"
    }
}

println "Conversion attempts:"
tryConvert("ACTIVE")     // Works
tryConvert("active")     // Case mismatch
tryConvert(" ACTIVE ")   // Whitespace
tryConvert("Active")     // Mixed case
tryConvert("UNKNOWN")    // Invalid value
tryConvert(null)         // Null input
tryConvert("")           // Empty string

Output

Conversion attempts:
  'ACTIVE' -> ACTIVE
  'active' -> FAILED: IllegalArgumentException - No enum constant Status.active
  ' ACTIVE ' -> FAILED: IllegalArgumentException - No enum constant Status. ACTIVE
  'Active' -> FAILED: IllegalArgumentException - No enum constant Status.Active
  'UNKNOWN' -> FAILED: IllegalArgumentException - No enum constant Status.UNKNOWN
  'null' -> FAILED: NullPointerException - Name is null
  '' -> FAILED: IllegalArgumentException - No enum constant Status.

Every one of these failures is something you’ll encounter in real applications. The rest of this post shows you how to handle them elegantly.

12 Practical Conversion Examples

Example 1: Safe Conversion with Default Value

What we’re doing: Converting a string to an enum safely, returning a default value instead of throwing an exception.

Example 1: Safe Conversion

enum Priority {
    LOW, MEDIUM, HIGH, CRITICAL

    static Priority fromString(String value, Priority defaultValue = null) {
        if (!value?.trim()) return defaultValue
        try {
            return valueOf(value.trim().toUpperCase())
        } catch (IllegalArgumentException e) {
            return defaultValue
        }
    }
}

// Successful conversions
println "Valid conversions:"
println "  'HIGH' -> ${Priority.fromString('HIGH')}"
println "  'high' -> ${Priority.fromString('high')}"
println "  ' Medium ' -> ${Priority.fromString(' Medium ')}"

println ""

// Failed conversions with defaults
println "With defaults:"
println "  'URGENT' -> ${Priority.fromString('URGENT', Priority.HIGH)}"
println "  null -> ${Priority.fromString(null, Priority.LOW)}"
println "  '' -> ${Priority.fromString('', Priority.MEDIUM)}"
println "  'invalid' -> ${Priority.fromString('invalid')}"

println ""

// Use in practice
def processTasks(List<Map> tasks) {
    tasks.each { task ->
        def priority = Priority.fromString(task.priority, Priority.MEDIUM)
        println "  Task '${task.name}': ${priority}"
    }
}

println "Processing tasks:"
processTasks([
    [name: "Fix bug", priority: "CRITICAL"],
    [name: "Update docs", priority: "low"],
    [name: "Refactor", priority: null],
    [name: "Review PR", priority: "URGENT"]
])

Output

Valid conversions:
  'HIGH' -> HIGH
  'high' -> HIGH
  ' Medium ' -> MEDIUM

With defaults:
  'URGENT' -> HIGH
  null -> LOW
  '' -> MEDIUM
  'invalid' -> null

Processing tasks:
  Task 'Fix bug': CRITICAL
  Task 'Update docs': LOW
  Task 'Refactor': MEDIUM
  Task 'Review PR': MEDIUM

What happened here: The fromString method handles all the common failure cases – null, empty, whitespace, and invalid values. It normalizes the input with trim().toUpperCase() and catches IllegalArgumentException to return a default value instead of crashing. This is the pattern you’ll use most often in production code.

Example 2: Case-Insensitive Conversion

What we’re doing: Building a case-insensitive enum lookup that handles any capitalization.

Example 2: Case-Insensitive Lookup

enum HttpMethod {
    GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS

    // Pre-built lookup map for O(1) case-insensitive access
    private static final Map<String, HttpMethod> LOOKUP =
        values().collectEntries { [it.name().toUpperCase(), it] }

    static HttpMethod fromString(String method) {
        if (!method?.trim()) return null
        return LOOKUP[method.trim().toUpperCase()]
    }
}

// Test various cases
def inputs = ["GET", "get", "Get", "post", "POST", "Put", " delete ", "INVALID", null]

inputs.each { input ->
    def result = HttpMethod.fromString(input)
    println "  '${input}' -> ${result ?: 'null'}"
}

println ""

// Practical use: parse HTTP requests
def parseRequest(String requestLine) {
    def parts = requestLine.split(/\s+/, 3)
    def method = HttpMethod.fromString(parts[0])
    def path = parts.size() > 1 ? parts[1] : "/"
    assert method : "Unknown HTTP method: ${parts[0]}"
    return [method: method, path: path]
}

def requests = [
    "GET /api/users HTTP/1.1",
    "post /api/users HTTP/1.1",
    "Delete /api/users/123 HTTP/1.1"
]

println "Parsed requests:"
requests.each { req ->
    def parsed = parseRequest(req)
    println "  ${parsed.method} ${parsed.path}"
}

Output

'GET' -> GET
  'get' -> GET
  'Get' -> GET
  'post' -> POST
  'POST' -> POST
  'Put' -> PUT
  ' delete ' -> DELETE
  'INVALID' -> null
  'null' -> null

Parsed requests:
  GET /api/users
  POST /api/users
  DELETE /api/users/123

What happened here: The pre-built LOOKUP map converts all enum names to uppercase once at class loading time. Then every lookup is a simple O(1) map access instead of iterating through all values. This is the recommended pattern when you have many lookups or many enum constants. The map is built using Groovy’s collectEntries – clean and expressive.

Example 3: Conversion by Display Name or Label

What we’re doing: Looking up an enum by a human-readable display name rather than the constant name.

Example 3: Lookup by Display Name

enum Country {
    US("United States", "USD"),
    UK("United Kingdom", "GBP"),
    DE("Germany", "EUR"),
    FR("France", "EUR"),
    JP("Japan", "JPY"),
    IN("India", "INR"),
    AU("Australia", "AUD")

    final String displayName
    final String currency

    Country(String displayName, String currency) {
        this.displayName = displayName
        this.currency = currency
    }

    // Lookup by multiple fields
    private static final Map<String, Country> BY_DISPLAY =
        values().collectEntries { [it.displayName.toUpperCase(), it] }

    private static final Map<String, Country> BY_CURRENCY =
        values().groupBy { it.currency }.collectEntries { k, v -> [k, v[0]] }

    static Country fromDisplayName(String name) {
        if (!name?.trim()) return null
        BY_DISPLAY[name.trim().toUpperCase()]
    }

    static Country fromCode(String code) {
        if (!code?.trim()) return null
        try { valueOf(code.trim().toUpperCase()) } catch (e) { null }
    }

    static Country fromCurrency(String currency) {
        BY_CURRENCY[currency?.toUpperCase()]
    }
}

// Lookup by different fields
println "By code: ${Country.fromCode('US')}"
println "By display: ${Country.fromDisplayName('United Kingdom')}"
println "By display (case): ${Country.fromDisplayName('germany')}"
println "By currency: ${Country.fromCurrency('JPY')}"

println ""

// Process user input
def userInputs = ["United States", "uk", "France", "GERMANY", "Brazil"]
println "User input mapping:"
userInputs.each { input ->
    def country = Country.fromDisplayName(input) ?: Country.fromCode(input)
    if (country) {
        println "  '${input}' -> ${country.name()} (${country.displayName}, ${country.currency})"
    } else {
        println "  '${input}' -> NOT FOUND"
    }
}

Output

By code: US
By display: United Kingdom
By display (case): Germany
By currency: Japan
By currency: JPY -> JP

User input mapping:
  'United States' -> US (United States, USD)
  'uk' -> UK (United Kingdom, GBP)
  'France' -> FR (France, EUR)
  'GERMANY' -> DE (Germany, EUR)
  'Brazil' -> NOT FOUND

What happened here: Real-world enums often need lookup by more than just their constant name. This pattern creates separate lookup maps for each field – display name, code, and currency. The fromDisplayName and fromCode methods try the user input against different fields. You can chain them with the Elvis operator: Country.fromDisplayName(input) ?: Country.fromCode(input).

Example 4: Groovy’s as Keyword with Enums

What we’re doing: Exploring Groovy’s as keyword for enum conversion and how it differs from valueOf().

Example 4: The ‘as’ Keyword

enum Size {
    SMALL, MEDIUM, LARGE, EXTRA_LARGE
}

// Direct 'as' conversion
def s1 = "SMALL" as Size
println "String as Size: ${s1} (${s1.class.simpleName})"

// 'as' is equivalent to valueOf()
assert ("LARGE" as Size) == Size.valueOf("LARGE")
println "Equivalence: passed"

// 'as' also fails on case mismatch
try {
    def s2 = "small" as Size  // Fails!
} catch (IllegalArgumentException e) {
    println "'small' as Size: FAILED (case-sensitive)"
}

// Use in method parameters with implicit conversion
def getDescription(Size size) {
    switch (size) {
        case Size.SMALL: return "Fits tightly"
        case Size.MEDIUM: return "Standard fit"
        case Size.LARGE: return "Relaxed fit"
        case Size.EXTRA_LARGE: return "Oversized fit"
    }
}

// 'as' in expressions
def sizes = ["SMALL", "MEDIUM", "LARGE"]
def converted = sizes.collect { it as Size }
println "Converted: ${converted}"
println "Types: ${converted.collect { it.class.simpleName }.unique()}"

println ""

// Safe 'as' conversion with helper
def safeAs(String value, Class<? extends Enum> enumClass, defaultValue = null) {
    if (!value?.trim()) return defaultValue
    try {
        return value.trim().toUpperCase() as Size
    } catch (IllegalArgumentException e) {
        return defaultValue
    }
}

println "Safe 'as': ${safeAs('medium', Size)}"
println "Safe 'as' null: ${safeAs(null, Size, Size.MEDIUM)}"
println "Safe 'as' invalid: ${safeAs('huge', Size, Size.LARGE)}"

Output

String as Size: SMALL (Size)
Equivalence: passed
'small' as Size: FAILED (case-sensitive)
Converted: [SMALL, MEDIUM, LARGE]
Types: [Size]

Safe 'as': MEDIUM
Safe 'as' null: MEDIUM
Safe 'as' invalid: LARGE

What happened here: Groovy’s as keyword provides a more Groovy-idiomatic way to convert strings to enums. Under the hood, it calls valueOf(), so it has the same case-sensitivity and error behavior. The as syntax is particularly nice in collect operations for batch conversion. But for production code, you still need the safe wrapper to handle invalid inputs.

Example 5: Enum Conversion with Aliases

What we’re doing: Supporting multiple string aliases that map to the same enum constant.

Example 5: Enum Aliases

enum FileType {
    IMAGE(["image", "img", "photo", "picture", "pic"]),
    VIDEO(["video", "vid", "movie", "clip"]),
    AUDIO(["audio", "sound", "music", "mp3"]),
    DOCUMENT(["document", "doc", "text", "txt", "pdf"]),
    ARCHIVE(["archive", "zip", "compressed", "tar"])

    final List<String> aliases

    FileType(List<String> aliases) {
        this.aliases = aliases
    }

    private static final Map<String, FileType> ALIAS_MAP = {
        def map = [:]
        values().each { type ->
            // Add the constant name itself
            map[type.name().toUpperCase()] = type
            // Add all aliases
            type.aliases.each { alias ->
                map[alias.toUpperCase()] = type
            }
        }
        return map
    }()

    static FileType fromString(String input) {
        if (!input?.trim()) return null
        ALIAS_MAP[input.trim().toUpperCase()]
    }
}

// Test with various inputs
def inputs = ["IMAGE", "photo", "Pic", "VIDEO", "clip", "mp3",
              "document", "PDF", "zip", "unknown"]

println "Alias-based conversion:"
inputs.each { input ->
    def type = FileType.fromString(input)
    println "  '${input}'.padRight(12) -> ${type ?: 'NOT FOUND'}"
}

println ""

// Classify files
def files = ["report.pdf", "vacation.jpg", "song.mp3", "backup.zip", "demo.mp4"]
println "File classification:"
files.each { file ->
    def ext = file.split(/\./)[-1]
    def type = FileType.fromString(ext)
    println "  ${file.padRight(16)} -> ${type ?: 'UNKNOWN'}"
}

Output

Alias-based conversion:
  'IMAGE'      -> IMAGE
  'photo'      -> IMAGE
  'Pic'        -> IMAGE
  'VIDEO'      -> VIDEO
  'clip'       -> VIDEO
  'mp3'        -> AUDIO
  'document'   -> DOCUMENT
  'PDF'        -> DOCUMENT
  'zip'        -> ARCHIVE
  'unknown'    -> NOT FOUND

File classification:
  report.pdf       -> DOCUMENT
  vacation.jpg     -> UNKNOWN
  song.mp3         -> AUDIO
  backup.zip       -> ARCHIVE
  demo.mp4         -> UNKNOWN

What happened here: The alias pattern is essential when your enum needs to accept many different string representations. Each constant stores a list of aliases. The static ALIAS_MAP is built once at class loading time by flattening all aliases into a single lookup map. This gives O(1) lookup regardless of how many aliases exist. The map includes both the constant name and all aliases, so "IMAGE" and "photo" both resolve correctly.

Example 6: Enum Conversion with Validation

What we’re doing: Converting strings to enums with validation, error collection, and batch processing.

Example 6: Validated Conversion

enum Role {
    ADMIN, EDITOR, VIEWER, GUEST

    static final Set<String> VALID_NAMES = values()*.name() as Set

    static boolean isValid(String input) {
        input?.trim()?.toUpperCase() in VALID_NAMES
    }

    static Role parse(String input) {
        if (!input?.trim()) {
            throw new IllegalArgumentException("Role cannot be null or empty")
        }
        def normalized = input.trim().toUpperCase()
        if (normalized !in VALID_NAMES) {
            throw new IllegalArgumentException(
                "Invalid role '${input}'. Valid roles: ${VALID_NAMES.join(', ')}"
            )
        }
        return valueOf(normalized)
    }
}

// Validate before converting
def inputs = ["ADMIN", "editor", "UNKNOWN", "", null, "viewer"]
println "Validation:"
inputs.each { input ->
    println "  '${input}' valid: ${Role.isValid(input)}"
}

println ""

// Parse with detailed error messages
println "Parsing:"
inputs.each { input ->
    try {
        def role = Role.parse(input)
        println "  '${input}' -> ${role}"
    } catch (IllegalArgumentException e) {
        println "  '${input}' -> ERROR: ${e.message}"
    }
}

println ""

// Batch validation
def batchConvert(List<String> inputs) {
    def results = [success: [], errors: []]
    inputs.each { input ->
        if (Role.isValid(input)) {
            results.success << [input: input, role: Role.valueOf(input.trim().toUpperCase())]
        } else {
            results.errors << [input: input, reason: "Invalid role value"]
        }
    }
    return results
}

def batch = batchConvert(["admin", "EDITOR", "superuser", "viewer", "root"])
println "Batch results:"
println "  Successful: ${batch.success.collect { it.role }}"
println "  Errors: ${batch.errors.collect { it.input }}"

Output

Validation:
  'ADMIN' valid: true
  'editor' valid: true
  'UNKNOWN' valid: false
  '' valid: false
  'null' valid: false
  'viewer' valid: true

Parsing:
  'ADMIN' -> ADMIN
  'editor' -> EDITOR
  'UNKNOWN' -> ERROR: Invalid role 'UNKNOWN'. Valid roles: ADMIN, EDITOR, VIEWER, GUEST
  '' -> ERROR: Role cannot be null or empty
  'null' -> ERROR: Role cannot be null or empty
  'viewer' -> VIEWER

Batch results:
  Successful: [ADMIN, EDITOR, VIEWER]
  Errors: [superuser, root]

What happened here: The isValid() method lets you check before converting – useful when you want to handle invalid values gracefully without exceptions. The parse() method provides detailed error messages that include the list of valid values, which is much more helpful than the default IllegalArgumentException message. The batch converter collects successes and errors separately, which is perfect for processing CSV imports or API bulk requests.

Example 7: Enum Conversion with Groovy’s find and findAll

What we’re doing: Using Groovy collection methods for flexible enum matching beyond exact string equality.

Example 7: Flexible Matching

enum LogLevel {
    TRACE(0), DEBUG(1), INFO(2), WARN(3), ERROR(4), FATAL(5)

    final int severity

    LogLevel(int severity) { this.severity = severity }
}

// find by name (case-insensitive)
def byName = LogLevel.values().find { it.name().equalsIgnoreCase("warn") }
println "Find by name: ${byName}"

// find by property
def bySeverity = LogLevel.values().find { it.severity == 3 }
println "Find by severity 3: ${bySeverity}"

// findAll - multiple matches
def highSeverity = LogLevel.values().findAll { it.severity >= 3 }
println "High severity: ${highSeverity}"

// Partial name matching
def partialMatch = LogLevel.values().find { it.name().startsWith("ER") }
println "Starts with 'ER': ${partialMatch}"

// Fuzzy matching with Levenshtein-like approach
def closestMatch(String input, Class<? extends Enum> enumClass) {
    def values = enumClass.values()
    def upper = input?.toUpperCase()

    // Try exact match first
    def exact = values.find { it.name() == upper }
    if (exact) return exact

    // Try contains
    def contains = values.find { it.name().contains(upper) || upper?.contains(it.name()) }
    if (contains) return contains

    // Try starts with
    def startsWith = values.find { it.name().startsWith(upper?.take(3) ?: "") }
    return startsWith
}

println ""
println "Fuzzy matching:"
["ERROR", "err", "WAR", "inf", "DEB", "FATAL"].each { input ->
    def match = closestMatch(input, LogLevel)
    println "  '${input}' -> ${match ?: 'NO MATCH'}"
}

println ""

// Convert list of strings to enums, filtering invalid ones
def rawInput = ["INFO", "debug", "INVALID", "warn", "ERROR", "unknown", "TRACE"]
def validLevels = rawInput.collect { name ->
    LogLevel.values().find { it.name().equalsIgnoreCase(name) }
}.findAll { it != null }

println "Filtered conversion: ${validLevels}"
println "Invalid count: ${rawInput.size() - validLevels.size()}"

Output

Find by name: WARN
Find by severity 3: WARN
High severity: [WARN, ERROR, FATAL]
Starts with 'ER': ERROR

Fuzzy matching:
  'ERROR' -> ERROR
  'err' -> ERROR
  'WAR' -> WARN
  'inf' -> INFO
  'DEB' -> DEBUG
  'FATAL' -> FATAL

Filtered conversion: [INFO, DEBUG, WARN, ERROR, TRACE]
Invalid count: 2

What happened here: Groovy’s collection methods make enum lookup extremely flexible. You can find by any criterion – name, property value, partial match, or any custom logic. The fuzzy matching example tries increasingly loose matches: exact, then contains, then prefix. The batch conversion pattern – collect then findAll to filter nulls – is a clean way to process lists of strings into enums.

Example 8: Enum Conversion with Groovy’s as Operator Customization

What we’re doing: Customizing enum coercion behavior by adding asType to String for custom enum conversion.

Example 8: Custom asType

enum TaskStatus {
    TODO("To Do"),
    IN_PROGRESS("In Progress"),
    IN_REVIEW("In Review"),
    DONE("Done"),
    CANCELLED("Cancelled")

    final String displayName

    TaskStatus(String displayName) {
        this.displayName = displayName
    }

    private static final Map<String, TaskStatus> LOOKUP = {
        def map = [:]
        values().each { status ->
            map[status.name().toUpperCase()] = status
            map[status.displayName.toUpperCase()] = status
            // Also add without underscores
            map[status.name().replaceAll('_', '').toUpperCase()] = status
            // And with spaces replaced by underscores
            map[status.displayName.replaceAll(' ', '_').toUpperCase()] = status
        }
        return map
    }()

    static TaskStatus fromString(String input) {
        if (!input?.trim()) return null
        def normalized = input.trim().toUpperCase()
        LOOKUP[normalized] ?: LOOKUP[normalized.replaceAll(/[\s_-]+/, '_')]
    }
}

// Multiple input formats, all resolving correctly
def inputs = [
    "TODO", "To Do", "to_do", "to-do",
    "IN_PROGRESS", "In Progress", "INPROGRESS", "in-progress",
    "IN_REVIEW", "In Review",
    "DONE", "Done",
    "CANCELLED", "Cancelled"
]

println "Flexible conversion:"
inputs.each { input ->
    def status = TaskStatus.fromString(input)
    println "  '${input}'.padRight(14) -> ${status?.name() ?: 'NOT FOUND'} (${status?.displayName ?: 'N/A'})"
}

println ""

// Real-world: parse task updates from different systems
def updates = [
    [task: "TASK-001", status: "In Progress"],   // From Jira
    [task: "TASK-002", status: "DONE"],           // From API
    [task: "TASK-003", status: "in_review"],      // From database
    [task: "TASK-004", status: "to-do"]           // From CSV import
]

println "Multi-system integration:"
updates.each { update ->
    def status = TaskStatus.fromString(update.status)
    println "  ${update.task}: ${status?.displayName ?: 'PARSE ERROR'}"
}

Output

Flexible conversion:
  'TODO'.padRight(14) -> TODO (To Do)
  'To Do'.padRight(14) -> TODO (To Do)
  'to_do'.padRight(14) -> TODO (To Do)
  'to-do'.padRight(14) -> TODO (To Do)
  'IN_PROGRESS'.padRight(14) -> IN_PROGRESS (In Progress)
  'In Progress'.padRight(14) -> IN_PROGRESS (In Progress)
  'INPROGRESS'.padRight(14) -> IN_PROGRESS (In Progress)
  'in-progress'.padRight(14) -> IN_PROGRESS (In Progress)
  'IN_REVIEW'.padRight(14) -> IN_REVIEW (In Review)
  'In Review'.padRight(14) -> IN_REVIEW (In Review)
  'DONE'.padRight(14) -> DONE (Done)
  'Done'.padRight(14) -> DONE (Done)
  'CANCELLED'.padRight(14) -> CANCELLED (Cancelled)
  'Cancelled'.padRight(14) -> CANCELLED (Cancelled)

Multi-system integration:
  TASK-001: In Progress
  TASK-002: Done
  TASK-003: In Review
  TASK-004: To Do

What happened here: When you integrate with multiple systems, enum values arrive in different formats – underscored, hyphenated, spaces, mixed case. The expanded lookup map registers every common variation: the constant name, the display name, without underscores, and with hyphens converted to underscores. This way, "In Progress", "IN_PROGRESS", "in-progress", and "INPROGRESS" all resolve to the same constant.

Example 9: Reverse Lookup by Property Value

What we’re doing: Converting a numeric code or other property value back to an enum constant.

Example 9: Reverse Lookup

enum ErrorCode {
    SUCCESS(0, "Operation completed successfully"),
    INVALID_INPUT(100, "Invalid input provided"),
    NOT_FOUND(404, "Resource not found"),
    UNAUTHORIZED(401, "Authentication required"),
    FORBIDDEN(403, "Access denied"),
    SERVER_ERROR(500, "Internal server error"),
    TIMEOUT(504, "Request timed out")

    final int code
    final String description

    ErrorCode(int code, String description) {
        this.code = code
        this.description = description
    }

    // Precomputed reverse lookup map
    private static final Map<Integer, ErrorCode> BY_CODE =
        values().collectEntries { [it.code, it] }

    static ErrorCode fromCode(int code) {
        BY_CODE[code]
    }

    static ErrorCode fromCode(String codeStr) {
        if (!codeStr?.trim()) return null
        try {
            fromCode(codeStr.trim().toInteger())
        } catch (NumberFormatException e) {
            null
        }
    }
}

// Lookup by integer code
println "By integer code:"
[0, 100, 404, 401, 500, 999].each { code ->
    def error = ErrorCode.fromCode(code)
    if (error) {
        println "  ${code} -> ${error.name()}: ${error.description}"
    } else {
        println "  ${code} -> UNKNOWN ERROR CODE"
    }
}

println ""

// Lookup by string code (from HTTP response, config file, etc.)
println "By string code:"
["404", "500", "200", "abc", null, ""].each { code ->
    def error = ErrorCode.fromCode(code)
    println "  '${code}' -> ${error?.name() ?: 'NOT FOUND'}"
}

println ""

// Practical: process API response codes
def responses = [
    [endpoint: "/api/users", code: 0],
    [endpoint: "/api/orders", code: 404],
    [endpoint: "/api/admin", code: 401],
    [endpoint: "/api/report", code: 504]
]

println "API Response Summary:"
responses.each { resp ->
    def error = ErrorCode.fromCode(resp.code)
    def status = error?.code == 0 ? "OK" : "FAIL"
    println "  ${resp.endpoint.padRight(16)} [${status}] ${error?.description ?: 'Unknown error'}"
}

Output

By integer code:
  0 -> SUCCESS: Operation completed successfully
  100 -> INVALID_INPUT: Invalid input provided
  404 -> NOT_FOUND: Resource not found
  401 -> UNAUTHORIZED: Authentication required
  500 -> SERVER_ERROR: Internal server error
  999 -> UNKNOWN ERROR CODE

By string code:
  '404' -> NOT_FOUND
  '500' -> SERVER_ERROR
  '200' -> NOT FOUND
  'abc' -> NOT FOUND
  'null' -> NOT FOUND
  '' -> NOT FOUND

API Response Summary:
  /api/users       [OK] Operation completed successfully
  /api/orders      [FAIL] Resource not found
  /api/admin       [FAIL] Authentication required
  /api/report      [FAIL] Request timed out

What happened here: Reverse lookup by numeric code is a very common pattern, especially with error codes and HTTP status codes. The BY_CODE map provides O(1) lookup. Notice the overloaded fromCode that accepts both int and String – the String version handles parsing and null safety. For similar string-to-number conversion patterns, see our String to Integer guide.

Example 10: Enum Conversion in switch Expressions

What we’re doing: Converting strings to enums within switch statements for clean dispatching logic.

Example 10: Enum in switch

enum Command {
    START, STOP, RESTART, STATUS, HELP

    static Command parse(String input) {
        if (!input?.trim()) return null
        try { valueOf(input.trim().toUpperCase()) } catch (e) { null }
    }
}

def executeCommand(String input) {
    def cmd = Command.parse(input)

    switch (cmd) {
        case Command.START:
            return "Starting service..."
        case Command.STOP:
            return "Stopping service..."
        case Command.RESTART:
            return "Restarting service..."
        case Command.STATUS:
            return "Service is running (uptime: 42 hours)"
        case Command.HELP:
            return "Available commands: ${Command.values()*.name().join(', ')}"
        case null:
            return "Unknown command: '${input}'. Type 'help' for available commands."
    }
}

// Process user commands
def commands = ["start", "STATUS", "help", "deploy", "Stop", "", "restart"]
println "Command processor:"
commands.each { cmd ->
    println "  > ${cmd ?: '(empty)'}"
    println "    ${executeCommand(cmd)}"
}

println ""

// Interactive-style processing with history
def history = []
["start", "status", "stop", "status", "start"].each { cmd ->
    def parsed = Command.parse(cmd)
    if (parsed) {
        history << parsed
        println "Executed: ${parsed}"
    }
}
println "Command history: ${history}"
println "Unique commands used: ${history.unique(false)}"

Output

Command processor:
  > start
    Starting service...
  > STATUS
    Service is running (uptime: 42 hours)
  > help
    Available commands: START, STOP, RESTART, STATUS, HELP
  > deploy
    Unknown command: 'deploy'. Type 'help' for available commands.
  > Stop
    Stopping service...
  > (empty)
    Unknown command: ''. Type 'help' for available commands.
  > restart
    Restarting service...

Executed: START
Executed: STATUS
Executed: STOP
Executed: STATUS
Executed: START
Command history: [START, STATUS, STOP, STATUS, START]
Unique commands used: [START, STATUS, STOP]

What happened here: The pattern here is parse-then-switch: convert the string to an enum first, then use the enum in a switch statement. If parsing fails, null is returned and the case null branch handles the error. This is cleaner than switching on raw strings because the compiler can help verify you’ve handled all cases. The command history example shows how enum values can be collected and analyzed.

Example 11: Generic Enum Converter

What we’re doing: Building a reusable, generic enum converter that works with any enum type.

Example 11: Generic Converter

class EnumConverter {
    // Basic conversion with case-insensitivity
    static <T extends Enum<T>> T convert(String value, Class<T> enumClass) {
        if (!value?.trim()) return null
        try {
            return Enum.valueOf(enumClass, value.trim().toUpperCase())
        } catch (IllegalArgumentException e) {
            return null
        }
    }

    // Conversion with default value
    static <T extends Enum<T>> T convert(String value, Class<T> enumClass, T defaultValue) {
        return convert(value, enumClass) ?: defaultValue
    }

    // Conversion with validation and error reporting
    static <T extends Enum<T>> Map convertWithInfo(String value, Class<T> enumClass) {
        def result = convert(value, enumClass)
        return [
            success: result != null,
            value: result,
            input: value,
            validValues: enumClass.values()*.name()
        ]
    }

    // Batch conversion
    static <T extends Enum<T>> List<T> convertAll(List<String> values, Class<T> enumClass) {
        values.collect { convert(it, enumClass) }.findAll { it != null }
    }
}

// Define some enums to test with
enum Color { RED, GREEN, BLUE, YELLOW }
enum Size { SMALL, MEDIUM, LARGE, EXTRA_LARGE }
enum Status { ACTIVE, INACTIVE, PENDING }

// Use the generic converter
println "Generic conversion:"
println "  Color: ${EnumConverter.convert('red', Color)}"
println "  Size: ${EnumConverter.convert('LARGE', Size)}"
println "  Status: ${EnumConverter.convert('pending', Status)}"
println "  Invalid: ${EnumConverter.convert('unknown', Color)}"

println ""

// With defaults
println "With defaults:"
println "  Color: ${EnumConverter.convert('invalid', Color, Color.RED)}"
println "  Size: ${EnumConverter.convert(null, Size, Size.MEDIUM)}"

println ""

// With info
def info = EnumConverter.convertWithInfo("purple", Color)
println "Conversion info:"
println "  Success: ${info.success}"
println "  Input: '${info.input}'"
println "  Valid values: ${info.validValues}"

println ""

// Batch conversion
def colorInputs = ["red", "BLUE", "purple", "GREEN", "orange", "yellow"]
def colors = EnumConverter.convertAll(colorInputs, Color)
println "Batch conversion: ${colorInputs} -> ${colors}"
println "Invalid count: ${colorInputs.size() - colors.size()}"

Output

Generic conversion:
  Color: RED
  Size: LARGE
  Status: PENDING
  Invalid: null

With defaults:
  Color: RED
  Size: MEDIUM

Conversion info:
  Success: false
  Input: 'purple'
  Valid values: [RED, GREEN, BLUE, YELLOW]

Batch conversion: [red, BLUE, purple, GREEN, orange, yellow] -> [RED, BLUE, GREEN, YELLOW]
Invalid count: 2

What happened here: The EnumConverter utility class works with any enum type thanks to generics. The <T extends Enum<T>> type parameter ensures type safety while keeping the code reusable. The convertWithInfo method returns a map with the result, the input, and the list of valid values – perfect for API error responses. The convertAll method handles batch conversion with automatic filtering of invalid values.

Example 12: Real-World Enum Conversion Patterns

What we’re doing: Practical scenarios combining enum conversion with configuration parsing, API responses, and data processing.

Example 12: Real-World Patterns

// Pattern 1: Parse configuration properties
enum LogLevel {
    TRACE, DEBUG, INFO, WARN, ERROR, FATAL

    static LogLevel fromConfig(String value) {
        if (!value?.trim()) return INFO  // Default
        try { valueOf(value.trim().toUpperCase()) } catch (e) { INFO }
    }
}

def config = [
    "app.log.level": "debug",
    "db.log.level": "WARN",
    "cache.log.level": "invalid",
    "api.log.level": null
]

println "Log level configuration:"
config.each { key, value ->
    def level = LogLevel.fromConfig(value)
    println "  ${key} = '${value}' -> ${level}"
}

println ""

// Pattern 2: Database row mapping
enum AccountType {
    CHECKING("C"), SAVINGS("S"), CREDIT("CR"), INVESTMENT("I")

    final String dbCode

    AccountType(String dbCode) { this.dbCode = dbCode }

    private static final Map<String, AccountType> BY_DB_CODE =
        values().collectEntries { [it.dbCode, it] }

    static AccountType fromDbCode(String code) {
        BY_DB_CODE[code?.toUpperCase()]
    }
}

// Simulate database rows
def dbRows = [
    [id: 1, name: "Main Checking", type_code: "C", balance: 5000],
    [id: 2, name: "Rainy Day Fund", type_code: "S", balance: 15000],
    [id: 3, name: "Visa Card", type_code: "CR", balance: -2500],
    [id: 4, name: "401k", type_code: "I", balance: 125000]
]

println "Account mapping from DB:"
dbRows.each { row ->
    def type = AccountType.fromDbCode(row.type_code)
    println "  ${row.name.padRight(18)} type=${type?.name()?.padRight(12)} balance=\$${row.balance}"
}

println ""

// Pattern 3: CSV import with error handling
enum Gender {
    MALE, FEMALE, NON_BINARY, PREFER_NOT_TO_SAY

    private static final Map<String, Gender> ALIASES = [
        "M": MALE, "F": FEMALE, "MALE": MALE, "FEMALE": FEMALE,
        "NB": NON_BINARY, "NON-BINARY": NON_BINARY, "NONBINARY": NON_BINARY,
        "X": PREFER_NOT_TO_SAY, "OTHER": PREFER_NOT_TO_SAY,
        "PREFER NOT TO SAY": PREFER_NOT_TO_SAY, "N/A": PREFER_NOT_TO_SAY
    ]

    static Gender fromCsv(String value) {
        if (!value?.trim()) return null
        ALIASES[value.trim().toUpperCase()]
    }
}

def csvRows = ["M", "Female", "NB", "other", "male", "non-binary", "X", "invalid"]
println "CSV gender import:"
csvRows.each { value ->
    def gender = Gender.fromCsv(value)
    println "  '${value}' -> ${gender ?: 'PARSE ERROR'}"
}

println ""

// Pattern 4: Mapping between enum systems
enum ExternalStatus {
    ACTIVE, SUSPENDED, CLOSED
}

enum InternalStatus {
    ENABLED, DISABLED, ARCHIVED

    static InternalStatus fromExternal(ExternalStatus ext) {
        switch (ext) {
            case ExternalStatus.ACTIVE: return ENABLED
            case ExternalStatus.SUSPENDED: return DISABLED
            case ExternalStatus.CLOSED: return ARCHIVED
            default: return DISABLED
        }
    }
}

println "Enum mapping:"
ExternalStatus.values().each { ext ->
    def internal = InternalStatus.fromExternal(ext)
    println "  External ${ext} -> Internal ${internal}"
}

Output

Log level configuration:
  app.log.level = 'debug' -> DEBUG
  db.log.level = 'WARN' -> WARN
  cache.log.level = 'invalid' -> INFO
  api.log.level = 'null' -> INFO

Account mapping from DB:
  Main Checking      type=CHECKING     balance=$5000
  Rainy Day Fund     type=SAVINGS      balance=$15000
  Visa Card          type=CREDIT       balance=$-2500
  401k               type=INVESTMENT   balance=$125000

CSV gender import:
  'M' -> MALE
  'Female' -> FEMALE
  'NB' -> NON_BINARY
  'other' -> PREFER_NOT_TO_SAY
  'male' -> MALE
  'non-binary' -> NON_BINARY
  'X' -> PREFER_NOT_TO_SAY
  'invalid' -> PARSE ERROR

Enum mapping:
  External ACTIVE -> Internal ENABLED
  External SUSPENDED -> Internal DISABLED
  External CLOSED -> Internal ARCHIVED

What happened here: Four real-world patterns. Config parsing converts property values to log levels with sensible defaults. Database mapping converts short codes stored in DB columns to rich enum objects. CSV import handles the wide variety of input formats users provide, using an alias map. Enum mapping bridges between external and internal systems where the enum names differ. These patterns show up constantly in production applications.

Building a Reliable Conversion Utility

Here’s a complete utility class you can use in your projects:

Reusable Enum Converter Utility

class EnumUtils {

    static <T extends Enum<T>> T valueOf(String name, Class<T> enumClass) {
        if (!name?.trim()) return null
        try {
            Enum.valueOf(enumClass, name.trim().toUpperCase())
        } catch (IllegalArgumentException e) {
            null
        }
    }

    static <T extends Enum<T>> T valueOfOrDefault(String name, Class<T> enumClass, T defaultValue) {
        valueOf(name, enumClass) ?: defaultValue
    }

    static <T extends Enum<T>> boolean isValid(String name, Class<T> enumClass) {
        valueOf(name, enumClass) != null
    }

    static <T extends Enum<T>> List<String> validNames(Class<T> enumClass) {
        enumClass.values()*.name()
    }

    static <T extends Enum<T>> List<T> parseAll(List<String> names, Class<T> enumClass) {
        names.collect { valueOf(it, enumClass) }.findAll { it != null }
    }
}

// Quick test
enum Fruit { APPLE, BANANA, CHERRY, DATE }

println "valueOf: ${EnumUtils.valueOf('banana', Fruit)}"
println "default: ${EnumUtils.valueOfOrDefault('mango', Fruit, Fruit.APPLE)}"
println "valid: ${EnumUtils.isValid('cherry', Fruit)}"
println "names: ${EnumUtils.validNames(Fruit)}"
println "parseAll: ${EnumUtils.parseAll(['apple', 'invalid', 'date'], Fruit)}"

Output

valueOf: BANANA
default: APPLE
valid: true
names: [APPLE, BANANA, CHERRY, DATE]
parseAll: [APPLE, DATE]

Enum Conversion with JSON and APIs

When working with JSON data from APIs, enum conversion becomes critical. Here’s how to handle it cleanly:

JSON Enum Handling

import groovy.json.JsonSlurper
import groovy.json.JsonOutput

enum OrderStatus {
    PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED

    static OrderStatus fromJson(String value) {
        if (!value?.trim()) return null
        try { valueOf(value.trim().toUpperCase()) } catch (e) { null }
    }
}

// Parse JSON with enum conversion
def json = '''[
    {"id": 1, "product": "Widget", "status": "pending"},
    {"id": 2, "product": "Gadget", "status": "SHIPPED"},
    {"id": 3, "product": "Doohickey", "status": "delivered"},
    {"id": 4, "product": "Thingamajig", "status": "UNKNOWN"}
]'''

def orders = new JsonSlurper().parseText(json).collect { raw ->
    [
        id: raw.id,
        product: raw.product,
        status: OrderStatus.fromJson(raw.status)
    ]
}

println "Parsed orders:"
orders.each { order ->
    def statusStr = order.status?.name() ?: "PARSE_ERROR"
    println "  #${order.id} ${order.product.padRight(14)} status=${statusStr}"
}

println ""

// Serialize back to JSON
def outputOrders = orders.findAll { it.status != null }
    .collect { [id: it.id, product: it.product, status: it.status.name()] }
def jsonOutput = JsonOutput.prettyPrint(JsonOutput.toJson(outputOrders))
println "Serialized JSON:"
println jsonOutput

Output

Parsed orders:
  #1 Widget         status=PENDING
  #2 Gadget         status=SHIPPED
  #3 Doohickey      status=DELIVERED
  #4 Thingamajig    status=PARSE_ERROR

Serialized JSON:
[
    {
        "id": 1,
        "product": "Widget",
        "status": "PENDING"
    },
    {
        "id": 2,
        "product": "Gadget",
        "status": "SHIPPED"
    },
    {
        "id": 3,
        "product": "Doohickey",
        "status": "DELIVERED"
    }
]

Performance Considerations

Enum conversion performance matters when processing thousands of records. Here’s what to know:

  • valueOf() is fast – Java caches a map internally, so it’s O(1) lookup
  • Pre-built lookup maps are fastest – build a Map<String, MyEnum> once and reuse it
  • values().find{} is O(n) – it iterates through all constants each time. Fine for small enums, avoid for tight loops with large enums
  • Try-catch is not expensive – catching IllegalArgumentException is fast for occasional misses. Only avoid it if the majority of inputs are invalid
  • String normalization has costtrim().toUpperCase() creates new String objects. For millions of conversions, consider caching normalized values

Best Practices

DO:

  • Always handle case-insensitivity – user input, config files, and API data can be any case
  • Provide a fromString() or parse() factory method on your enum for clean conversion
  • Use pre-built lookup maps for O(1) access when you have many conversions or large enums
  • Return null or a default value for invalid inputs – don’t force callers to catch exceptions
  • Trim whitespace before conversion – it’s the most common silent failure cause
  • Include valid values in error messages to help users correct their input

DON’T:

  • Use valueOf() directly on user input without wrapping it – it will throw on invalid values
  • Depend on ordinal() for conversion – it breaks when you reorder or add constants
  • Swallow exceptions silently without logging – at least log unknown values at debug level
  • Create separate conversion logic in every class that uses the enum – centralize it in the enum itself
  • Assume API consumers will send exact enum constant names – always normalize input

Conclusion

We covered every way to convert a String to an Enum in Groovy – from the basic valueOf() and as keyword to case-insensitive lookups, alias maps, reverse lookups by property, generic converters, and real-world patterns for JSON, databases, and CSV imports.

The core lesson: never use valueOf() directly on untrusted input. Always wrap it with normalization (trim, uppercase) and error handling (try-catch with a default value). Pre-built lookup maps give you O(1) performance and the flexibility to support aliases and display names. Keep your conversion logic inside the enum itself as a static factory method so it’s easy to find and maintain.

For the complete guide to enums themselves, see our Groovy Enum guide. For other type conversion patterns, check our String to Integer guide.

Summary

  • valueOf() and as are case-sensitive – always normalize input first
  • Create a static fromString() method on your enum for safe, case-insensitive conversion
  • Use pre-built Map<String, MyEnum> for O(1) lookup performance
  • Support aliases and display names for user-friendly input handling
  • Return null or a default value instead of throwing exceptions for invalid input

If you also work with build tools, CI/CD pipelines, or cloud CLIs, check out Command Playground to practice 105+ CLI tools directly in your browser — no install needed.

Up next: Groovy Metaprogramming – Runtime Guide

Frequently Asked Questions

Is valueOf() case-sensitive in Groovy enums?

Yes, valueOf() is case-sensitive. Calling Color.valueOf('red') will throw IllegalArgumentException because the constant is ‘RED’. Always convert to uppercase with toUpperCase() before calling valueOf(), or create a fromString() factory method that handles case normalization.

How do I convert a string to an enum without throwing an exception?

Wrap valueOf() in a try-catch and return null or a default value on failure. Create a static fromString() method: static MyEnum fromString(String s) { try { valueOf(s?.trim()?.toUpperCase()) } catch (e) { null } }. This handles null, whitespace, case differences, and invalid values.

What is the difference between valueOf() and the ‘as’ keyword for enums?

They behave identically. ‘BLUE’ as Color calls Color.valueOf('BLUE') internally. Both are case-sensitive and throw IllegalArgumentException for invalid values. The ‘as’ keyword is more Groovy-idiomatic and reads better in collection operations like collect { it as Color }.

How do I look up an enum by a property value instead of its name?

Create a static lookup map using collectEntries: private static final Map BY_CODE = values().collectEntries { [it.code, it] }. Then add a static method: static MyEnum fromCode(int code) { BY_CODE[code] }. This gives O(1) reverse lookup by any property.

Can I convert a string to an enum case-insensitively in Groovy?

Yes, by normalizing the input before conversion. The simplest approach: try { valueOf(input.trim().toUpperCase()) } catch (e) { null }. For better performance, build a pre-computed Map with all names uppercased: values().collectEntries { [it.name().toUpperCase(), it] } and look up in that map.

Previous in Series: Groovy Enum – Constants, Methods, and Best Practices

Next in Series: Groovy Metaprogramming – Runtime Guide

Related Topics You Might Like:

This post is part of the Groovy & Grails Cookbook series on TechnoScripts.com

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

Your email address will not be published. Required fields are marked *