Chuyện gì vừa xảy ra
Bạn cố lưu một document và MongoDB ném ra lỗi này:
MongoServerError: The dotted field 'user.name' in 'data.user.name' is not valid for storage.
Nguyên nhân là một quy tắc cơ bản của MongoDB: ký hiệu dấu chấm (parent.child) là cách MongoDB duyệt qua các document lồng nhau. Khi chính tên field chứa dấu chấm, MongoDB không thể phân biệt giữa một đường dẫn lồng nhau và một tên key theo nghĩa đen — vì vậy nó từ chối lưu trữ hoàn toàn.
Chín trên mười trường hợp, lỗi này xuất phát từ dữ liệu đầu vào của người dùng chưa được làm sạch, một lần import CSV/JSON, hoặc response từ API bên thứ ba. Các key trông ổn trong code của bạn, nhưng đâu đó trên đường đi chúng lén mang theo một dấu chấm.
Tái hiện vấn đề
Đây là trường hợp tối giản trong Node.js:
// Node.js — mongodb driver
const doc = {
data: {
'user.name': 'Alice' // dấu chấm nằm trong tên key!
}
};
await collection.insertOne(doc);
// → MongoServerError: The dotted field 'user.name' in 'data.user.name'
// is not valid for storage.
Không chỉ riêng insertOne. Lỗi tương tự cũng xảy ra với updateOne, findOneAndUpdate, replaceOne — bất kỳ thao tác ghi nào truyền vào một document chứa key có dấu chấm, dù lồng sâu đến đâu.
Tìm vị trí các dấu chấm
Trước khi chọn cách sửa, hãy xác định tất cả các key có vấn đề. Một hàm đệ quy nhỏ có thể làm điều này trong vài giây:
// JavaScript — tìm tất cả key có dấu chấm trong một object
function findDottedKeys(obj, path = '') {
for (const key of Object.keys(obj)) {
const fullPath = path ? `${path}.${key}` : key;
if (key.includes('.')) {
console.warn('Dotted key found:', fullPath);
}
if (obj[key] !== null && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
findDottedKeys(obj[key], fullPath);
}
}
}
findDottedKeys(doc);
// Dotted key found: data.user.name
Chạy hàm này trên payload trước khi lưu trong lúc debug. Khi đã biết phạm vi — một field hay năm mươi — hãy chọn cách tiếp cận phù hợp dưới đây.
Cách sửa 1 — Thay dấu chấm trong tên key bằng ký tự an toàn
Thay mọi dấu chấm trong tên key bằng dấu gạch dưới (hoặc ký tự phân cách khác) trước khi ghi vào MongoDB:
// JavaScript — làm sạch key theo đệ quy
function sanitizeKeys(obj) {
if (Array.isArray(obj)) {
return obj.map(sanitizeKeys);
}
if (obj !== null && typeof obj === 'object') {
return Object.fromEntries(
Object.entries(obj).map(([k, v]) => [
k.replace(/\./g, '_'), // dấu chấm → dấu gạch dưới
sanitizeKeys(v)
])
);
}
return obj;
}
const safe = sanitizeKeys(doc);
await collection.insertOne(safe);
Chọn ký tự phân cách không trùng với tên key thực. Các lựa chọn phổ biến: _, dấu chấm giữa · (U+00B7), hoặc escape Unicode như \u2024. Dù chọn gì, hãy ghi lại — bạn sẽ cần mapping ngược khi đọc dữ liệu trở lại.
Cách sửa 2 — Tái cấu trúc thành document lồng nhau đúng chuẩn
Đôi khi một key có dấu chấm như user.name vốn dĩ được thiết kế để là một field lồng nhau. Nếu đúng vậy, hãy làm cho cấu trúc lồng nhau tường minh:
// Trước (sai)
const bad = { data: { 'user.name': 'Alice' } };
// Sau (cấu trúc lồng nhau đúng)
const good = { data: { user: { name: 'Alice' } } };
await collection.insertOne(good);
Đây là lựa chọn gọn gàng nhất khi dữ liệu nguồn chỉ đơn giản là được định dạng kỳ lạ. Thêm vào đó, các câu truy vấn trở nên tự nhiên hơn — { 'data.user.name': 'Alice' } hoạt động đúng như MongoDB mong muốn.
Cách sửa 3 — Lưu các key động dưới dạng mảng cặp key-value
Có tên field thực sự không thể đoán trước — form submissions, analytics events, config do người dùng định nghĩa? Đừng cố chống lại quy tắc key của MongoDB. Hãy dùng mảng thay thế:
// Thay vì: { 'user.name': 'Alice', 'user.email': 'a@b.com' }
// Lưu như sau:
const doc = {
attributes: [
{ key: 'user.name', value: 'Alice' },
{ key: 'user.email', value: 'a@b.com' }
]
};
await collection.insertOne(doc);
Truy vấn sẽ dài dòng hơn ({ attributes: { $elemMatch: { key: 'user.name', value: 'Alice' } } }), nhưng schema luôn hợp lệ bất kể key nào đến từ nguồn bên ngoài. Với các tập thuộc tính có độ phân tán cao, cách này còn tránh được tình trạng index bùng nổ.
Python / pymongo
Lỗi giống nhau, nguyên nhân gốc rễ giống nhau:
# pymongo
from pymongo import MongoClient
client = MongoClient('mongodb://localhost:27017/')
col = client.mydb.mycollection
doc = {'data': {'user.name': 'Alice'}}
col.insert_one(doc)
# raises: pymongo.errors.WriteError:
# The dotted field 'user.name' in 'data.user.name' is not valid for storage.
Cách sửa trong Python tương tự như JavaScript:
def sanitize_keys(obj):
if isinstance(obj, list):
return [sanitize_keys(i) for i in obj]
if isinstance(obj, dict):
return {
k.replace('.', '_'): sanitize_keys(v)
for k, v in obj.items()
}
return obj
safe_doc = sanitize_keys(doc)
col.insert_one(safe_doc)
Mongoose (Node.js ODM)
Đang dùng Mongoose với field Mixed hoặc Schema.Types.Mixed? Thêm một pre-save hook để việc làm sạch dữ liệu tự động xảy ra trên mọi lần ghi:
schema.pre('save', function (next) {
if (this.data) {
this.data = sanitizeKeys(this.data);
this.markModified('data');
}
next();
});
Xác minh bản sửa lỗi
Ba kiểm tra nhanh xác nhận mọi thứ đã hoạt động:
// 1. Insert một document thử nghiệm
const result = await collection.insertOne(sanitizeKeys(testDoc));
console.log('Inserted ID:', result.insertedId);
// 2. Đọc lại dữ liệu
const stored = await collection.findOne({ _id: result.insertedId });
console.log(JSON.stringify(stored, null, 2));
// Các key phải hiển thị dấu gạch dưới (hoặc cấu trúc lồng nhau), không có dấu chấm
// 3. Xác nhận không còn key nào có dấu chấm
findDottedKeys(stored); // Không in ra gì cả
Insert thành công, document được lưu trông đúng, và findDottedKeys im lặng — bạn đã xong.
Phòng ngừa từ đầu nguồn
- Kiểm tra tại điểm biên — từ chối hoặc làm sạch các key có dấu chấm trước khi chúng đến tầng database. Các schema validator như Joi hoặc Zod có thể áp dụng quy tắc định dạng key tại điểm vào API, nơi việc phát hiện lỗi có chi phí thấp nhất.
- Không để lộ key do người dùng định nghĩa trực tiếp — nếu người dùng có thể đặt tên field của riêng họ, hãy dùng Cách sửa 3 (pattern mảng
{ key, value }) thay vì để các chuỗi tùy ý trở thành key của document. - MongoDB 5.0+ đã nới lỏng một phần hạn chế này — dấu chấm và ký hiệu dollar được phép trong tên field trong một số ngữ cảnh aggregation thông qua các toán tử như
$setField. Tuy nhiên, insert/update thông thường vẫn bị từ chối ở cấp driver. Hãy làm sạch trong ứng dụng của bạn; đừng dựa vào hành vi của server theo từng phiên bản cụ thể.

