Converting Naming Styles: snake_case, camelCase, Constants

2026-11-057 min readDeveloper Tools4 tools covered

The same field is user_name in the database, userName in Java and USER_NAME as a constant. Converting between those styles is easy; deciding where one word ends and the next begins is the hard part.

Key takeaways
  • Conversion splits on underscore, dash, space and camelCase boundaries but never between a letter and a digit, which keeps sha256Sum intact instead of turning it into sha_256_sum
  • All-caps acronyms are treated as one word, so the result is httpUrl rather than hTTPUrl - preserve a specific casing by hand
  • Generated entities need field names in the target language style while serialisation tags keep the original key, otherwise deserialisation breaks
  • A nullable database column should map to a union with null in TypeScript rather than an optional property, since the two mean different things
  • Fallback types must match the target language; returning String everywhere produces a boxed object in TypeScript rather than a primitive

#Where naming conversion gets hard

The difficulty is not uppercase versus lowercase, it is deciding how to split a name into words. Split wrong once and every generated field name is wrong.

The mapping between the common styles is fixed:

StyleExampleTypical use
snake_caseuser_nameDatabase columns, environment variables
camelCaseuserNameJava and JavaScript variables
PascalCaseUserNameClass and interface names, React components
SCREAMING_SNAKEUSER_NAMEConstants, enum values
kebab-caseuser-nameCSS classes, URL slugs

The difficulty sits entirely in boundary detection: r and N in userName form a boundary, the underscore in user_name is a boundary, and sha256Sum?

#Word boundaries: digits are not one

A letter-to-digit change does not count as a word boundary, and that is what keeps sha256, base64 and utf8 intact. Treating digits as boundaries produces sha_256_sum, which is wrong.

The rules used by the naming converter:

  • Counts as a boundary: underscore, dash, space, and a camelCase case change
  • Does not count: between a letter and a digit

So sha256Sum becomes sha256_sum, and v2 becomes v2 (or V2 in constant style, never V_2). Those two examples are the fastest way to judge whether a converter is any good.

#Handling all-caps acronyms

A run of the same case is read as one word, so HTTPURL becomes httpurl while HTTPUrl becomes httpUrl. There is no single correct way to split acronyms, which is why tools disagree.

The reliable answer is to write names in the target style at the source: httpUrl splits well, HTTPURL does not, and userId beats userID. Put "acronyms in lower case except the first letter" into the naming guideline and conversion stops surprising you.

Note

Conversion handles ASCII letters only. With Chinese or other non-ASCII input the tool says plainly that there is nothing convertible rather than quietly returning empty strings - if the result is empty, check whether the input was English naming at all.

#Generated code: field names and serialisation tags

When generating entities, field names follow the target language convention while serialisation tags must keep the original key - otherwise the JSON coming back will not deserialise. This is the trap automatic generators fall into most often.

With the JSON to TypeScript tool the key handling has two layers:

  • Invalid identifiers get quoted automatically: {"user-name": 1} becomes "user-name": number; rather than user-name: number;, the latter being a syntax error
  • The root interface name is sanitised: my-type becomes MyType

With the SQL to entity tool, Go output carries a json:"original_key" tag - the field name becomes camelCase while the tag keeps the column name. Both must be present, or renaming a field breaks deserialisation.

Warning

Generated code is a starting point. The type map covers common types only (integers, decimals, strings, timestamps, booleans). Custom database types, enums, JSON columns and arrays usually come out as fallback types and need manual work.

#Nullable fields: optional property or union with null

"The column is nullable" and "the client may omit this field" are different statements, and TypeScript should not express them the same way. Conflating them makes type checking meaningless.

The correct mapping:

  • Column allows NULL - the field always exists, the value may be null: field: string | null
  • API may omit the field - the field may not exist: field?: string

The SQL to entity tool uses the first form, because a table column always exists and only its value may be absent. Expressing that as an optional property makes "not sent" and "sent as null" indistinguishable.

Tip

A practical test: does this field always appear in the data? Always present but possibly empty means a union with null; possibly absent entirely means an optional property. The same test applies to hand-written interfaces.

#Fallback types belong to the target language

Fallbacks must follow the target language. Returning one type everywhere breaks some languages - in TypeScript, returning String with a capital S yields a boxed object, not a primitive.

Sensible fallbacks per language:

  • TypeScript: unknown, which forces the caller to narrow
  • Java: Object, or a concrete wrapper class
  • Go: interface{} or any
  • Python: no annotation, or Any

Also remember the generator does not infer semantics: JSON null maps to a null type, array element types are unioned ([1,"a"] becomes (number | string)[]), and an empty array becomes unknown[]. All correct, all worth checking against the real contract.

#Tidy the formatting too

Once names are consistent, formatting should be consistent as well - both are team style, and mixing them turns code review into an argument. Run the formatter after generating or editing code.

With the Python formatter, know what it does and what it refuses to do:

  • Does: unify indentation to 4 spaces by block structure, pad operators, indent continuation lines one level inside unclosed brackets
  • Does not: expand single-line compound statements (if x: y=1 stays as written)
  • Does not: clean up inner bracket spacing (f( a , b ) is preserved)

The minify direction leaves indentation alone, because in Python indentation is syntax and one pass can change behaviour.

Key point

Formatting assumes the code parses. A missing colon, unbalanced brackets or broken block structure will only be rearranged more neatly, not fixed. Get the code running first, then talk about style.

#Which tool to use

What you needToolKey constraint
snake_case to camelCase or backNaming converterDigits are not boundaries; digit-bearing words stay whole
Turn JSON into TypeScript interfacesJSON to TypeScriptInvalid keys get quoted; same-name clashes are split
Turn DDL into entity classesSQL to entityHandles decimal, enum, comments with commas, composite keys
Generate Java or Go entitiesJSON to entity classesGo output keeps original keys in json tags
Normalise Python indentation and spacingPython formatterNo expansion of one-liners; minify leaves indentation

Suggested process: write the naming guideline down, use converters only to migrate existing code, and write new code to the guideline directly. Repeated conversion accumulates inconsistency; the guideline is what actually fixes it.

#Frequently asked questions

#Why is sha256Sum not converted to sha_256_sum?

Because a letter-to-digit change is not a word boundary. That is deliberate: the digits in sha256, base64, utf8 and sha1 are part of the name, and splitting them is wrong. If you genuinely need digit splitting, handle those few cases by hand.

#The generated TypeScript has quoted field names. Can I remove the quotes?

No, and you should not. The quotes indicate the key is not a valid JavaScript identifier - it contains a dash, a space, or starts with a digit - so removing them is a syntax error. Fix the source data keys instead of the type definition.

#Should a nullable column use ? or a union with null?

A union with null. ? means the field may be absent, while a database column always exists and only its value may be empty. Using ? makes "field not returned" indistinguishable from "returned as null", and the first case should be an error rather than a branch.

#Why did two nested objects with the same name produce two interfaces?

Because their shapes differ. The generator deduplicates on name plus structure, so when data.address and meta.address disagree it emits Address and Address2, each referenced correctly. Forcing them into one interface gives the referring property the wrong type.

#Formatting changed Python indentation - will it still run?

It might not, and that can be fatal, since Python expresses block structure through indentation. The tool re-indents by keyword and bracket depth, so code that relies on indentation for meaning (such as content inside a multi-line string) could be altered. Commit before formatting so the change is easy to compare.

Tools used in this guide

Related guides