Casts and mutators allow you to automatically transform data as it moves between your application and the database. Casts handle type conversions, while accessors and mutators let you customize how attributes are read and written.
Type Casts
Casts automatically convert data types when reading from or writing to the database. Define them in the _casts property:
class User extends Model {
// Define type conversions
}
User . _table = "Users" ;
User . _casts = {
age: "int" ,
verified: "bool" ,
metadata: "json" ,
settings: "array" ,
birthdate: "date" ,
created_at: "datetime" ,
score: "float"
};
User . use ( db );
Reading Data with Casts
const user = User . find ( 1 );
Logger . log ( typeof user . age ); // "number"
Logger . log ( typeof user . verified ); // "boolean"
Logger . log ( typeof user . metadata ); // "object"
Logger . log ( user . birthdate ); // Date object
Writing Data with Casts
user . metadata = { theme: "dark" , lang: "es" };
user . save ();
// Saved to sheet as JSON string: '{"theme":"dark","lang":"es"}'
user . verified = true ;
user . save ();
// Saved to sheet as boolean: true
Available Cast Types
From Model.js:57-107, ServiceSQL supports these cast types:
Cast Type Description Input Example Output Example int, integerInteger number "25"25float, doubleDecimal number "3.14"3.14bool, booleanBoolean "true", 1truestringString 123"123"jsonJSON object '{"a":1}'{a:1}arrayArray '[1,2,3]'[1,2,3]date, datetimeDate object "2025-01-15"Date objectdmydd/mm/yyyy format Date"15/01/2025"dmyhmsFull date+time Date"15/01/2025 10:30:00"
Cast Implementation
The casting logic from Model.js:57-107:
static _castValue ( value , type ) {
if ( value === null || value === undefined ) return value ;
switch ( type ) {
case "int" :
return Number ( value );
case "integer" :
return Number . parseInt ( value ) || 0 ;
case "float" :
case "double" :
return Number . parseFloat ( value ) || 0 ;
case "bool" :
case "boolean" :
return (
value === true || value === "true" || value === 1 || value === "1"
);
case "json" :
try {
return typeof value === "string" ? JSON . parse ( value ) : value ;
} catch ( e ) {
return value ;
}
case "array" :
try {
return Array . isArray ( value ) ? value : JSON . parse ( value );
} catch ( e ) {
return value ;
}
case "date" :
case "datetime" :
return new Date ( value );
case "dmy" :
const d = new Date ( value );
return ` ${ d . getDate () } / ${ d . getMonth () + 1 } / ${ d . getFullYear () } ` ;
case "dmyhms" :
const dt = new Date ( value );
return ` ${ dt . getDate () } / ${ dt . getMonth () + 1 } / ${ dt . getFullYear () } ${ dt . getHours () } : ${ dt . getMinutes () } : ${ dt . getSeconds () } ` ;
default :
return value ;
}
}
Practical Cast Examples
User Model with Multiple Casts
class User extends Model {}
User . _table = "Users" ;
User . _casts = {
age: "int" ,
admin: "bool" ,
balance: "float" ,
preferences: "json" ,
tags: "array" ,
created_at: "datetime"
};
User . use ( db );
// Create user
const user = User . create ({
name: "John" ,
age: "30" , // String → Number
admin: "true" , // String → Boolean
balance: "99.99" , // String → Float
preferences: { theme: "dark" }, // Object → JSON string in DB
tags: [ "developer" , "admin" ] // Array → JSON string in DB
});
Logger . log ( typeof user . age ); // "number"
Logger . log ( typeof user . admin ); // "boolean"
Logger . log ( typeof user . balance ); // "number"
Logger . log ( typeof user . preferences ); // "object"
Logger . log ( Array . isArray ( user . tags )); // true
Settings with JSON Cast
class AppSettings extends Model {}
AppSettings . _table = "Settings" ;
AppSettings . _casts = {
config: "json"
};
AppSettings . use ( db );
// Create settings
const settings = AppSettings . create ({
name: "app_config" ,
config: {
theme: "light" ,
notifications: true ,
features: [ "chat" , "video" ]
}
});
// Read settings
const loaded = AppSettings . find ( settings . id );
Logger . log ( loaded . config . theme ); // "light"
Logger . log ( loaded . config . features [ 0 ]); // "chat"
Accessors (Getters)
Accessors transform attributes when reading them. Define them as methods with the pattern getXxxAttribute:
class User extends Model {
// Combine first and last name
getFullNameAttribute ( value ) {
return ` ${ this . first_name } ${ this . last_name } ` ;
}
// Normalize email to lowercase
getEmailAttribute ( value ) {
return value . toLowerCase ();
}
// Format age with text
getAgeInYearsAttribute ( value ) {
return ` ${ this . age } years old` ;
}
}
User . _table = "Users" ;
User . use ( db );
const user = User . find ( 1 );
Logger . log ( user . full_name ); // "John Doe"
Logger . log ( user . email ); // "[email protected] " (lowercase)
Logger . log ( user . age_in_years ); // "30 years old"
Accessor Implementation
From Model.js:43-52, accessors are applied during model hydration:
// apply accessors
for ( const k in cleaned ) {
const method =
"get" + k . charAt ( 0 ). toUpperCase () + k . slice ( 1 ) + "Attribute" ;
if ( typeof inst [ method ] === "function" ) {
try {
inst [ k ] = inst [ method ]( inst [ k ]);
} catch ( e ) {}
}
}
Accessor Examples
class Product extends Model {
getFinalPriceAttribute ( value ) {
return this . price - ( this . price * this . discount / 100 );
}
getIsOnSaleAttribute ( value ) {
return this . discount > 0 ;
}
}
Product . _table = "Products" ;
Product . use ( db );
const product = Product . first ();
Logger . log ( product . final_price ); // 89.99 (computed from price and discount)
Logger . log ( product . is_on_sale ); // true
Mutators (Setters)
Mutators transform attributes when writing them. Define them as methods with the pattern setXxxAttribute:
class User extends Model {
// Trim and uppercase names
setNameAttribute ( value ) {
return value . trim (). toUpperCase ();
}
// Normalize email
setEmailAttribute ( value ) {
return value . toLowerCase (). trim ();
}
// Hash password before saving
setPasswordAttribute ( value ) {
return hashPassword ( value );
}
}
User . _table = "Users" ;
User . use ( db );
const user = new User ();
user . name = " john doe " ;
user . email = " [email protected] " ;
user . save ();
// Saved to database:
// name: "JOHN DOE"
// email: "[email protected] "
Mutator Implementation
From Model.js:365-387, mutators are applied before saving:
static _prepareForSave ( data ) {
const ModelClass = this ;
const out = Object . assign ({}, data );
for ( const key in out ) {
// Apply mutator
const method =
"set" + key . charAt ( 0 ). toUpperCase () + key . slice ( 1 ) + "Attribute" ;
if ( typeof ModelClass . prototype [ method ] === "function" ) {
try {
out [ key ] = ModelClass . prototype [ method ]( out [ key ]);
} catch ( e ) { /* ignore */ }
}
// Apply cast for saving
const cast = ModelClass . _casts && ModelClass . _casts [ key ];
if ( cast ) {
if ( cast === "json" || cast === "array" )
out [ key ] = JSON . stringify ( out [ key ]);
else if ( cast === "date" || cast === "datetime" )
out [ key ] = new Date ( out [ key ]). toISOString ();
}
}
return out ;
}
Mutator Examples
class User extends Model {
setPhoneAttribute ( value ) {
// Remove all non-digit characters
return value . replace ( / \D / g , '' );
}
setUsernameAttribute ( value ) {
// Lowercase and remove spaces
return value . toLowerCase (). replace ( / \s / g , '' );
}
}
User . _table = "Users" ;
User . use ( db );
const user = User . create ({
phone: "(555) 123-4567" , // Saved as: "5551234567"
username: "John Doe" // Saved as: "johndoe"
});
class User extends Model {
setPasswordAttribute ( value ) {
// Hash password with bcrypt
return bcrypt . hashSync ( value , 10 );
}
setSsnAttribute ( value ) {
// Encrypt sensitive data
return encrypt ( value , SECRET_KEY );
}
}
User . _table = "Users" ;
User . use ( db );
const user = User . create ({
password: "mypassword123" // Saved as hashed value
});
Combining Casts, Accessors, and Mutators
You can use all three together for powerful data transformation:
class Product extends Model {
// Mutator: normalize price before saving
setPriceAttribute ( value ) {
return parseFloat ( value ). toFixed ( 2 );
}
// Accessor: format price for display
getPriceFormattedAttribute ( value ) {
return `$ ${ this . price } ` ;
}
}
Product . _table = "Products" ;
Product . _casts = {
price: "float" ,
stock: "int" ,
active: "bool" ,
metadata: "json"
};
Product . use ( db );
const product = Product . create ({
name: "Widget" ,
price: "19.99999" // Mutator → "19.99", Cast → 19.99
});
Logger . log ( product . price ); // 19.99 (float)
Logger . log ( product . price_formatted ); // "$19.99" (accessor)
Best Practices
Use casts for type safety
Define casts for all fields that need type conversion: User . _casts = {
age: "int" ,
verified: "bool" ,
settings: "json"
};
Use mutators for data normalization
Ensure data is clean before saving: setEmailAttribute ( value ) {
return value . toLowerCase (). trim ();
}
Use accessors for computed properties
Calculate derived values without storing them: getFullNameAttribute ( value ) {
return ` ${ this . first_name } ${ this . last_name } ` ;
}
Don't mutate original data unnecessarily
Only use mutators when you need to transform data for storage: // ✅ Good: transform for storage
setPasswordAttribute ( value ) {
return hashPassword ( value );
}
// ❌ Bad: side effects
setNameAttribute ( value ) {
sendNotification ( `Name changed to ${ value } ` );
return value ;
}
Common Pitfalls
Avoid these mistakes:
Circular dependencies : Don’t reference other accessors within an accessor
Performance issues : Accessors run every time you access a property, so avoid expensive operations
Mutator side effects : Mutators should only transform data, not trigger other actions
Cast conflicts : Don’t define both a cast and mutator for the same field unless necessary
Summary
Feature Purpose Pattern Example Casts Type conversion _casts objectage: "int"Accessors Transform on read getXxxAttribute()getFullNameAttribute()Mutators Transform on write setXxxAttribute()setEmailAttribute()
Casts, accessors, and mutators make your models self-documenting and ensure data consistency throughout your application. Use them to keep your business logic centralized in your models.