Reference
camelCase in TypeScript, snake_case in the database
Field names are converted to columns when a statement is built and converted back when a row is hydrated. Knowing which side of that boundary you are on removes most naming surprises.
Verified against s-m-r-t 0.42.4
One rule, applied at the database boundary
Converting to a column inserts an underscore before each capital and lowercases the result. Converting back uppercases the letter after each underscore. The conversion runs where a query is built and where a row is hydrated; nothing else in the stack re-cases keys.
@smrt({ api: true })
export class Invoice extends SmrtObject {
customerId = ''; // column: customer_id
apiKey = ''; // column: api_key
totalCents = 0; // column: total_cents
}
// Table name comes from the class: invoicesWhere each spelling appears
Use column names in one location and field names in another. The correct name depends on whether the framework generated the surface or you wrote the SQL.
- Model properties, where keys, orderBy fields, and select entries: the name declared on the class.
- Database columns, indexes, and constraints: snake_case.
- Generated REST request bodies, responses, and filter query parameters: the declared field name, as in ?status=open&total[gte]=100.
- Generated MCP tool input schemas: the declared field name.
- Generated CLI flags: --declaredFieldName, taken verbatim as a payload key, so a kebab-case spelling will not resolve to a field.
- SQL text passed to collection.query(): column names. The returned rows are hydrated back to field names.
Table names follow a different rule
A separate conversion changes a class name to a table name. It splits only where a lowercase letter meets a capital. Then, it pluralizes the last word. Item becomes items, JournalEntry becomes journal_entries, and Currency becomes currencies.
- An all-caps prefix has no lowercase-to-uppercase boundary, so the class APIKey becomes apikeys even though its apiKey field becomes api_key. The two conversions are not the same function.
- Set tableName on @smrt() when the derived name is not the one you want. There is no per-field column-name override — a column name always follows from the field name.
Edge cases in the conversion
The conversion uses two small regular expressions, not a dictionary. Thus, some shapes have results that you can misremember.
- Consecutive capitals are split individually: pdfURL becomes pdf_u_r_l. The round trip back to pdfURL is exact, but the column is harder to read and to type in hand-written SQL — prefer pdfUrl.
- A field the model declares in snake_case stays that way through hydration: a declared publish_date is returned as publish_date rather than being renamed to publishDate.
- The inherited system fields are literally snake_case in TypeScript too: created_at and updated_at, alongside id, slug, and context. Reading object.created_at and sorting by created_at DESC are both correct.
- Framework fields that start with an underscore keep the prefix through the column mapping, so _metaType addresses the _meta_type column.
Filtering across the conversion
Query options take field names and are converted for you, which is also where a misspelling is caught.
- where keys are validated against the model fields after conversion, so a typo reports the valid field names rather than failing as a SQL error.
- orderBy is converted as well, so createdAt DESC and created_at DESC both resolve to the same column.
- Fields marked sensitive are rejected as filter keys regardless of which spelling is used.
- list({ select }) takes field names and returns rows keyed by those same names, so a projection never leaks column spellings into page code.