Converting Naming Styles: snake_case, camelCase, Constants
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.
- 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:
| Style | Example | Typical use |
|---|---|---|
| snake_case | user_name | Database columns, environment variables |
| camelCase | userName | Java and JavaScript variables |
| PascalCase | UserName | Class and interface names, React components |
| SCREAMING_SNAKE | USER_NAME | Constants, enum values |
| kebab-case | user-name | CSS 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.
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 thanuser-name: number;, the latter being a syntax error - The root interface name is sanitised:
my-typebecomesMyType
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.
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.
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{}orany - 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=1stays 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.
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 need | Tool | Key constraint |
|---|---|---|
| snake_case to camelCase or back | Naming converter | Digits are not boundaries; digit-bearing words stay whole |
| Turn JSON into TypeScript interfaces | JSON to TypeScript | Invalid keys get quoted; same-name clashes are split |
| Turn DDL into entity classes | SQL to entity | Handles decimal, enum, comments with commas, composite keys |
| Generate Java or Go entities | JSON to entity classes | Go output keeps original keys in json tags |
| Normalise Python indentation and spacing | Python formatter | No 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
Convert to camelCase, PascalCase, snake_case, kebab-case
Open tool →JSON to TypeScriptGenerate TypeScript interfaces from JSON
Open tool →SQL to EntityGenerate Java / C# / TypeScript entities from CREATE TABLE
Open tool →Python FormatterPEP8-style Python code beautifier and minifier
Open tool →