Sequelize: How to Return Only Database Results as JSON Objects (Excluding Extra Metadata)
Sequelize is a powerful Object-Relational Mapper (ORM) for Node.js, widely used to interact with relational databases like PostgreSQL, MySQL, and SQLite. It simplifies database operations by mapping database tables to JavaScript objects (models) and providing methods for querying, inserting, updating, and deleting data. However, a common frustration among developers is that Sequelize queries often return results wrapped in extra metadata (e.g., dataValues, _previousDataValues, isNewRecord), which can clutter API responses or complicate data processing.
If you’ve ever tried to send Sequelize query results directly to a client (e.g., via an Express API) and ended up with bloated JSON containing Sequelize-specific fields, this blog is for you. We’ll explore actionable methods to strip out metadata and return clean, plain JSON objects from Sequelize queries. By the end, you’ll know exactly how to customize your results to include only the data you need.
Table of Contents#
-
Understanding the Problem: Sequelize’s Default Response
- Example: Default Query Result with Metadata
- Why Extra Metadata Is a Problem
-
Solutions to Return Clean JSON Objects
- Method 1: Using the
toJSON()Method - Method 2: Using
{ raw: true }in Queries - Method 3: Using
get({ plain: true })on Instances - Method 4: Combining
attributeswithrawfor Specific Columns - Method 5: Handling Associations (Nested Results)
- Method 6: Overriding
toJSON()for Security (Excluding Sensitive Data)
- Method 1: Using the
-
- When to Use Each Method
- Performance Considerations
- Avoiding Sensitive Data Leaks
1. Understanding the Problem: Sequelize’s Default Response#
By default, Sequelize returns query results as Sequelize instances (objects with additional methods and metadata), not plain JavaScript objects. These instances include properties like dataValues (the actual database data), _previousDataValues, isNewRecord, _options, and more. While this metadata is useful for Sequelize’s internal operations (e.g., tracking changes for updates), it’s rarely needed when returning data to a client.
Example: Default Query Result with Metadata#
Let’s define a simple User model and query it to see the default output:
// User model definition
const { Model, DataTypes } = require('sequelize');
const sequelize = new Sequelize('sqlite::memory:');
class User extends Model {}
User.init({
name: DataTypes.STRING,
email: DataTypes.STRING,
age: DataTypes.INTEGER
}, { sequelize });
// Sync model and query
(async () => {
await sequelize.sync();
await User.create({ name: 'Alice', email: '[email protected]', age: 30 });
const users = await User.findAll();
console.log(users);
})(); The output will look something like this (simplified for clarity):
[
User {
dataValues: { id: 1, name: 'Alice', email: '[email protected]', age: 30, createdAt: '2024-01-01', updatedAt: '2024-01-01' },
_previousDataValues: { id: 1, name: 'Alice', email: '[email protected]', age: 30, createdAt: '2024-01-01', updatedAt: '2024-01-01' },
isNewRecord: false,
_options: { ... },
// ... other metadata properties
}
] Notice the dataValues object contains the actual database data, but it’s nested inside a Sequelize User instance with extra fields.
Why Extra Metadata Is a Problem#
- Bloated Responses: Clients receive unnecessary data, increasing payload size and slowing down APIs.
- Security Risks: Metadata might accidentally expose internal implementation details (e.g.,
_optionscould reveal query logic). - Inconsistent Data Structures: Clients expect predictable JSON, but Sequelize instances add unexpected fields.
2. Solutions to Return Clean JSON Objects#
Let’s explore proven methods to strip metadata and return plain JSON objects from Sequelize queries.
Method 1: Using the toJSON() Method#
Every Sequelize instance has a built-in toJSON() method that returns the dataValues object (the clean database data) and excludes metadata. This is the simplest way to convert instances to plain objects.
How to Use:#
Call toJSON() on individual instances or map over an array of instances:
// Query and convert to clean JSON
const users = await User.findAll();
const cleanUsers = users.map(user => user.toJSON());
console.log(cleanUsers);
// Output: [{ id: 1, name: 'Alice', email: '[email protected]', age: 30, createdAt: '2024-01-01', updatedAt: '2024-01-01' }] When to Use:#
- You need Sequelize instances for other operations (e.g., calling instance methods like
update()ordestroy()) before converting to JSON. - You want to retain Sequelize’s built-in handling of associations (more on this later).
Method 2: Using { raw: true } in Queries#
The raw: true option tells Sequelize to return plain JavaScript objects instead of Sequelize instances. This skips instance creation entirely, making queries faster and results cleaner.
How to Use:#
Pass { raw: true } in the query options:
const users = await User.findAll({ raw: true });
console.log(users);
// Output: [{ id: 1, name: 'Alice', email: '[email protected]', age: 30, createdAt: '2024-01-01', updatedAt: '2024-01-01' }] Key Notes:#
- Works with all query methods:
findAll,findOne,findByPk,findAndCountAll, etc. - Performance: Faster than
toJSON()because it avoids creating Sequelize instances. - Limitation: Does not return Sequelize instances, so you can’t call instance methods (e.g.,
user.update()) on the results.
Method 3: Using get({ plain: true }) on Instances#
If you already have a Sequelize instance (e.g., from create, update, or findOne), use instance.get({ plain: true }) to extract a plain object.
How to Use:#
// Get an instance
const user = await User.findByPk(1);
// Convert to plain object
const cleanUser = user.get({ plain: true });
console.log(cleanUser);
// Output: { id: 1, name: 'Alice', email: '[email protected]', age: 30, createdAt: '2024-01-01', updatedAt: '2024-01-01' } When to Use:#
- You need the instance for operations like validation or updates before converting to JSON.
- Useful for single instances (e.g.,
findOneresults).
Method 4: Combining attributes with raw for Specific Columns#
To return only specific columns (e.g., exclude createdAt/updatedAt), use the attributes option with raw: true.
How to Use:#
const users = await User.findAll({
attributes: ['id', 'name', 'email'], // Only return these columns
raw: true
});
console.log(users);
// Output: [{ id: 1, name: 'Alice', email: '[email protected]' }] Key Notes:#
attributescontrols which columns are returned.- Combine with
raw: trueto avoid Sequelize instances.
Method 5: Handling Associations#
Associations (e.g., hasMany, belongsTo) complicate metadata removal because Sequelize nests related data in instances. Let’s use a Post model associated with User to demonstrate:
// Define Post model with association
class Post extends Model {}
Post.init({
title: DataTypes.STRING,
content: DataTypes.TEXT
}, { sequelize });
User.hasMany(Post); // User has many Posts
Post.belongsTo(User); // Post belongs to User
await sequelize.sync(); Problem: Associations with raw: true#
By default, raw: true flattens associated data into a single object, which is often undesirable:
const usersWithPosts = await User.findAll({
include: [{ model: Post }],
raw: true
});
// Output (flattened, hard to parse):
// [{ id: 1, name: 'Alice', 'Posts.id': 1, 'Posts.title': 'Hello World' }, ...] Solution 1: Use { raw: true, nest: true }#
Add nest: true to nest associated data under a key (e.g., Posts):
const usersWithPosts = await User.findAll({
include: [{ model: Post }],
raw: true,
nest: true // Nests associated data
});
console.log(usersWithPosts);
// Output (nested):
// [{
// id: 1,
// name: 'Alice',
// Posts: [{ id: 1, title: 'Hello World', content: '...' }]
// }] Solution 2: Use toJSON() for Nested Associations#
toJSON() automatically nests associated data, making it cleaner than raw: true for complex relationships:
const usersWithPosts = await User.findAll({
include: [{ model: Post }]
});
// Convert instances to JSON (nests Posts automatically)
const cleanUsers = usersWithPosts.map(user => user.toJSON());
console.log(cleanUsers);
// Output (nested, same as nest: true but with Sequelize's handling):
// [{
// id: 1,
// name: 'Alice',
// Posts: [{ id: 1, title: 'Hello World', content: '...' }]
// }] Key Takeaway:#
Use { raw: true, nest: true } for simple associations or toJSON() for complex nested relationships.
Method 6: Overriding toJSON() to Exclude Sensitive Data#
Sequelize instances include all columns by default (e.g., password in a user model). To exclude sensitive data, override the toJSON() method in your model.
How to Use:#
class User extends Model {
// Override toJSON to exclude sensitive fields
toJSON() {
const values = { ...this.get() }; // Get all dataValues
delete values.password; // Remove sensitive field
return values;
}
}
User.init({
name: DataTypes.STRING,
email: DataTypes.STRING,
password: DataTypes.STRING // Sensitive field
}, { sequelize });
// Now, even if you return the instance directly, password is excluded
const user = await User.findByPk(1);
console.log(user.toJSON());
// Output: { id: 1, name: 'Alice', email: '[email protected]', createdAt: '...', updatedAt: '...' } (no password) Critical Security Note:#
Always exclude sensitive data (passwords, tokens) via toJSON() or attributes to prevent leaks in API responses.
3. Best Practices#
| Method | Use Case | Pros | Cons |
|---|---|---|---|
toJSON() | Instances needed for methods + associations | Handles nested associations cleanly | Slower than raw: true (instance creation) |
{ raw: true } | Simple queries, performance-critical | Fast, no instance overhead | No instance methods, associations require nest: true |
get({ plain: true }) | Single instances (e.g., findOne) | Flexible for instance operations | Only for individual instances |
attributes + raw | Specific columns only | Control over returned data | Requires manual column selection |
Key Takeaways:#
- Prioritize
raw: truefor simple queries where you don’t need Sequelize instances. - Use
toJSON()when working with associations or when you need instances for other operations. - Override
toJSON()in models to exclude sensitive data (e.g., passwords).
4. Troubleshooting Common Issues#
- Associations Not Nesting: Use
{ raw: true, nest: true }ortoJSON()to fix flattened results. - Sensitive Data Leaks: Always override
toJSON()or useattributesto exclude fields likepassword. - Metadata Still Present: Ensure you’re using the correct method (e.g.,
raw: truein the query options, not as a second argument).
5. Conclusion#
Sequelize’s default metadata can be a nuisance, but with methods like raw: true, toJSON(), and get({ plain: true }), you can easily return clean JSON objects. Choose the method based on your use case: raw: true for performance, toJSON() for associations, and get({ plain: true }) for individual instances. Always override toJSON() to exclude sensitive data, and test thoroughly with associations to ensure nested results are formatted correctly.