
DoltHub is the version-control database company. We support version-controlled SQL databases that are clones of databases you already know: Dolt for MySQL, Doltgres for PostgreSQL, and DoltLite for SQLite. We now support a MongoDB clone named DumboDB, which this post is about.
DumboDB has recently had several improvements in its ability to support “validators.” Validators are how you enforce schema in MongoDB. There are a few edge cases introduced by the fact that DumboDB supports branching and merging, and we’ll talk about all of that today. Let’s go!
Validators, a Review#
A validator is a set of rules attached to a collection that dictates what document shapes, field types, and values are allowed to be written. Because MongoDB, and therefore DumboDB, is “schemaless”, validators are the primary mechanism for ensuring data integrity across application deployments, preventing bad or unexpected data from creeping into your database.
MongoDB accomplishes this using JSON Schema validation via the $jsonSchema operator during initial creation (createCollection) or collection updates (collMod).
For example, createCollection can take a second positional argument, which is a document that contains the validator. The following example creates the users collection which requires all documents to have a name (string with 1 or more chars) and email (string with @ in between two other strings.)
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: [ "name", "email" ],
properties: {
name: {
bsonType: "string",
minLength: 1,
description: "must be a string and is required"
},
email: {
bsonType: "string",
pattern: "^.+@.+$",
description: "must be a valid email string and is required"
}
}
}
},
validationLevel: "strict",
validationAction: "error"
})
You can come up with a better regular expression for emails to match RFC 5322, but you get the idea. If you attempt to create or update a document and it doesn’t contain either of these fields, or the strings provided do not live up to the requirements, you will get an error.
Note that these rules are applied only at the time you insert or update any document. So if you create a collection with no validators at all, create any number of documents, then update the validators with collMod; the existing documents will remain untouched, possibly invalid. This applies to modified validators as well - if you change a validator to be more restrictive, that new definition will not be applied to existing documents. Grandfathered documents will exist in your database until you fix them.
validationLevel and validationAction#
To manage how strictly schema rules apply, especially when dealing with grandfathered documents, we have two variables:
validationLevel:Controls which documents are checked during write operations.- strict (Default): Applies validation rules to all inserts and updates across the board.
- moderate: Applies validation checks to all inserts, but only enforces rules on updates to existing documents that already satisfy the validator. If a legacy document is already invalid, minor updates to it won’t be blocked unless you touch fields that break the schema further.
- off: Disables validation enforcement entirely without deleting the underlying schema rules. This is generally toggled when operational events require it. It effectively disables the validator entirely.
validationAction: Controls what happens when a document fails validation.- error (Default): Rejects the write operation completely and throws an error to the client.
- warn: Allows the write operation to succeed anyway, but logs a warning about the schema violation to the server log.
These flags give you the flexibility to keep your application running while increasing safety. Ultimately, migrating all of your documents to be valid is the goal, but you may want to use validationAction = warn for a period of time to ensure you don’t see anything in the logs indicating you have stale documents.
Finally, by performing a find for the documents which were grandfathered in, you can find documents which don’t match the $jsonSchema, like so:
db.users.find({
$nor: [ // Use the "nor" matcher to find all documents which are invalid.
{
$jsonSchema: { // Use the same $jsonSchema as in the collection definition.
bsonType: "object",
required: [ "name", "email" ],
properties: {
name: {
bsonType: "string",
minLength: 1
},
email: {
bsonType: "string",
pattern: "^.+@.+$"
}
}
}
}
]
})
Then you can fix them and toggle your validationAction accordingly once they are all fixed.
Now with Version Control#
Version control adds a wrinkle to this story.
We have two entities that we are versioning - data and the validator. It’s possible to add/modify/delete data on any branch, and it’s also possible to add/modify/delete the validator on any branch.
One basic use case for this is to see what you have changed. Using the collMod command, you can change the validator on a branch, and then use dumboDiff to see what has changed:
db.runCommand({
collMod: "users",
validator: {
$jsonSchema: {
bsonType: "object",
required: [ "name", "email" ],
properties: {
name: { bsonType: "string", minLength: 1 },
email: { bsonType: "string", pattern: "^.+@.+\\.com$" } // Stricter regex
}
}
}
})
validators> db.runCommand({dumboDiff:1})
{
changes: [
{
type: 'collection',
name: 'users',
status: 'modified',
documents: { added: [], removed: [], modified: [] },
indexes: { added: [], removed: [], modified: [] },
metadata: {
from: {
validator: {
'$jsonSchema': {
bsonType: 'object',
properties: {
email: {
bsonType: 'string',
description: 'must be a valid email string and is required',
pattern: '^.+@.+$'
},
name: {
bsonType: 'string',
description: 'must be a string and is required',
minLength: 1
}
},
required: [ 'name', 'email' ]
}
},
validationLevel: 'strict',
validationAction: 'error'
},
to: {
validator: {
'$jsonSchema': {
bsonType: 'object',
properties: {
email: { bsonType: 'string', pattern: '^.+@.+\\.com$' },
name: { bsonType: 'string', minLength: 1 }
},
required: [ 'name', 'email' ]
}
},
validationLevel: 'strict',
validationAction: 'error'
}
}
}
],
ok: 1
}
In the dumboDiff output, you can see that the users collection has been modified, and the metadata field shows the from and to validators, which are different. In fact, it shows that we’ve lost the description fields. This is because the collMod command only allows you to set the validator, and not modify it, so we neglected to include the descriptions. This is a good example of how an operator can verify their changes before committing them to the database. The operator can re-run the collMod command with the descriptions included and then run dumboDiff again to verify that the changes are correct.
Once you are happy with the changes, commit them:
db.users.updateOne(
{ email: "alice@example.org" },
{ $set: { email: "alice@example.com" } }
);
db.dumbo.commit({ message: "Require .com emails and migrate existing users" });
Merging Branches with Validators#
As stated above, DumboDB tracks both the validators and the data fully under version control. In practice, this means that validators can be changed on any branch, and when branches are merged, the validators are merged as well. This can lead to some interesting situations.
Say the main branch has a collection with a validator and all documents are valid in the collection. The feature branch starts at main, and you add several documents which adhere to the validator. At the same time, the stricter branch also starts at main, but the validator has been modified to be more strict. For example, for the email field, the regex has been changed to require a .com at the end (the example above). This stricter validator update is made at the same time as when you fix the documents on the stricter branch, so stricter is internally consistent. You merge stricter into main, because the migration is done.
What happens when you merge feature into main? The documents added or modified in the feature branch are valid according to the validator in feature, but they may not be valid according to the validator in main.
We can actually get more complicated than this. What if the definition of the validator is changed in both branches? There is a three-way-merge conflict between the two validators, which requires its own conflict resolution. Once the resolution is done, either branch could have invalid documents in the other branch. DumboDB provides a workflow to resolve this situation!
Two Phase Conflict Resolution#
DumboDB handles this situation with a two-phase conflict resolution process. When you attempt to merge two branches, the first phase is to merge the validators. If there is a conflict, you will need to use dumboResolveConflict to resolve the conflict. Once the validators are merged, DumboDB will check all altered documents in the merged branch against the new validator. If any documents are invalid, you will need to fix them before you can complete the merge. Again, the dumboResolveConflict method is used for this purpose.
Furthermore, the validationLevel and validationAction flags are used as part of the merge process. For example, if you have validationAction = warn, then merging in documents which don’t pass validation will result in warnings in your log files, not a merge conflict. For this reason, it is not recommended to use anything other than validationAction = error for long-running branches. It’s best to use it for the duration of a migration, then switch it to error.
Let’s run through a full example which has both a validator conflict and a document conflict. This is an admittedly long example, so I’ll be using closed code blocks to spare you lots of scrolling. Expand the piece you want to see.
First, we create a new DumboDB database and create a collection with a validator. We’ll use the email validator from above, which requires a name and an email field, and the email must have an @ in it:
Click to view code
use validators
validators> db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: [ "name", "email" ],
properties: {
name: {
bsonType: "string",
minLength: 1,
description: "must be a string and is required"
},
email: {
bsonType: "string",
pattern: "^.+@.+$",
description: "must be a valid email string and is required"
}
}
}
},
validationLevel: "strict",
validationAction: "error"
})
Let’s add a few documents to the collection, and commit them to the main branch:
Click to view code
validators> db.users.insertMany([
{ name: "Alice", email: "alice@example.com" },
{ name: "Bob", email: "bob@example.org" },
{ name: "Charlie", email: "charlie@example.net" },
{ name: "Diana", email: "diana@example.com" },
{ name: "Evan", email: "evan@example.io" }
]);
validators> db.runCommand({ dumboCommit: 1, message: "Initial commit with valid user documents" });
That went well because all of the documents are valid according to the validator. Now let’s create a new branch called feature and add a few more documents, all of which are valid:
Click to view code
validators> db.runCommand({ dumboBranch: 1, branch: "feature" });
validators> feature = db.getSiblingDB("validators@feature");
validators> feature.users.insertMany([
{ name: "Fiona", email: "fiona@example.org" },
{ name: "George", email: "george@example.com" }
]);
validators> feature.runCommand({ dumboCommit: 1, message: "Add feature branch users" });
At the same time, the validator is made a little more strict on the feature branch. Specifically, we want to require that the email domain has at least one dot in it, so we change the regex to ^.+@.+\..+$. This is a valid change, and all of the documents in the feature branch are still valid according to the new validator.
Click to view code
validators> feature.runCommand({
collMod: "users",
validator: {
$jsonSchema: {
bsonType: "object",
required: [ "name", "email" ],
properties: {
name: {
bsonType: "string",
minLength: 1,
description: "must be a string and is required"
},
email: {
bsonType: "string",
pattern: "^.+@.+\\..+$",
description: "must be a valid email string and is required"
}
}
}
}
});
validators> feature.runCommand({ dumboCommit: 1, message: "Update validator to require domain extension" });
Now, back on the main branch, someone has decided to make a stricter validation of the email. They only want to allow .com. Capitalism, am I right? Using the collMod command and updating all documents to have a .com email, as follows:
Click to view code
validators> db.users.updateMany(
{ email: { $not: /\.com$/ } },
[{ $set: { email: { $concat: [ { $arrayElemAt: [ { $split: [ "$email", "@" ] }, 0 ] }, "@example.com" ] } } }]
);
validators> db.runCommand({
collMod: "users",
validator: {
$jsonSchema: {
bsonType: "object",
required: [ "name", "email" ],
properties: {
name: {
bsonType: "string",
minLength: 1,
description: "must be a string and is required"
},
email: {
bsonType: "string",
pattern: "^.+@.+\\.com$",
description: "must be a valid .com email string and is required"
}
}
}
}
});
validators> db.runCommand({ dumboCommit: 1, message: "Migrate non-.com emails and update validator to require .com" });
To recap: we have two branches, feature and main. The validator definition has been updated on both branches, but in different ways, and each collection is valid according to its own validator.
Now, let’s try to merge feature into main. This will result in a validator conflict, because the two branches have different definitions for the email field.
Click to view code
validators> db.runCommand({ dumboMerge: 1, mergeIn: "feature" });
MongoServerError: dumboMerge: unresolved conflicts in 1 collection(s)
validators> db.runCommand({dumboConflicts: 1})
{
conflicts: [
{
conflictId: '6M7oxkLunq36FV0/LmGv/g',
type: 'metadata',
name: 'users',
reason: {
code: 'bothModified',
message: `branch 'main' (ours) and branch 'feature' (theirs) both changed t
he validator/options of "users"`
},
base: {
validator: {
'$jsonSchema': {
bsonType: 'object',
properties: {
email: {
bsonType: 'string',
description: 'must be a valid email string and is required',
pattern: '^.+@.+$'
},
name: {
bsonType: 'string',
description: 'must be a string and is required',
minLength: 1
}
},
required: [ 'name', 'email' ]
}
},
validationLevel: 'strict',
validationAction: 'error'
},
ours: {
validator: {
'$jsonSchema': {
bsonType: 'object',
properties: {
email: {
bsonType: 'string',
description: 'must be a valid .com email string and is required',
pattern: '^.+@.+\\.com$'
},
name: {
bsonType: 'string',
description: 'must be a string and is required',
minLength: 1
}
},
required: [ 'name', 'email' ]
}
},
validationLevel: 'strict',
validationAction: 'error',
diffType: 'modified'
},
theirs: {
validator: {
'$jsonSchema': {
bsonType: 'object',
properties: {
email: {
bsonType: 'string',
description: 'must be a valid email string and is required',
pattern: '^.+@.+\\..+$'
},
name: {
bsonType: 'string',
description: 'must be a string and is required',
minLength: 1
}
},
required: [ 'name', 'email' ]
}
},
validationLevel: 'strict',
validationAction: 'error',
diffType: 'modified'
}
}
],
ok: 1
}
Using the dumboConflicts command, we see that the users collection has a conflict in the validator. The base is the original validator, while ours is the most restrictive (requires .com rather than just a .). To resolve the conflict, we resolve using ours because we feel stricter is better. When we continue the merge, the documents being merged in will validate based on the validator after the conflict is resolved. Note that this applies to the diff being merged in, so only the newly created and updated documents will be checked against the new validator.
validators> db.runCommand({
dumboResolveConflict: 1,
conflictId: "6M7oxkLunq36FV0/LmGv/g",
resolution: "ours"
});
validators> db.runCommand({ dumboMerge: 1, continue: 1 });
MongoServerError: dumboMerge: unresolved conflicts in 1 collection(s)
This is an important point. The validator conflict was correctly resolved, but now we have a document conflict. The feature branch added a document that passes the feature validator but fails the main validator. This is a document conflict and must be resolved before the merge can continue.
validators> db.runCommand({dumboConflicts: 1})
{
conflicts: [
{
conflictId: 'DwnghEBfJ2py0+cLXBBZFA',
type: 'validation', // This document fails with new updated validator.
collection: 'users',
documentId: ObjectId('6a83976b44cc060b1e80ec81'),
reason: {
code: 'documentValidationFailure',
message: `document ObjectId('6a83976b44cc060b1e80ec81') in "users" violates
the collection validator merged from branch 'feature' (theirs)`
},
document: {
_id: ObjectId('6a83976b44cc060b1e80ec81'),
email: 'fiona@example.org',
name: 'Fiona'
},
[...snip...]
This conflict is resolved by updating the document to pass the validator. In this case, we will update the email in each document to have a .com domain.
Click to view code
// 1. Fetch current unresolved conflicts
let res = db.runCommand({ dumboConflicts: 1 });
// 2. Filter for validation conflicts and loop through them
if (res.ok && res.conflicts) {
res.conflicts.forEach(c => {
if (c.type === 'validation' && c.document) {
// Copy the original document from the conflict payload
let updatedDoc = Object.assign({}, c.document);
// Replace the domain part of the email with example.com
if (updatedDoc.email) {
let prefix = updatedDoc.email.split('@')[0];
updatedDoc.email = prefix + "@example.com";
}
// Resolve the conflict using the 'custom' resolution payload
db.runCommand({
dumboResolveConflict: 1,
conflictId: c.conflictId,
resolution: "custom",
value: updatedDoc
});
}
});
}
// 3. Resume the merge operation once all conflicts are resolved
db.runCommand({ dumboMerge: 1, continue: 1 });
{
commitId: 'b8v3b0dm61vk0l19nr6sa7070h4p3cl6',
message: "Merge branch 'feature' into 'main'",
author: 'dumbodb <dumbodb@dumbodb>',
timestamp: ISODate('2026-08-18T17:07:19.575Z'),
committer: 'dumbodb <dumbodb@dumbodb>',
committerTimestamp: ISODate('2026-08-18T17:07:19.575Z'),
ok: 1
}
Now that the merge is complete, we can verify that all documents in the users collection are valid according to the new validator. Note the shortcut here of getting the active schema from the collection options, rather than copying the $jsonSchema manually.
// 1. Fetch the collection metadata to get the active validator schema
validators> let infos = db.getCollectionInfos({ name: "users" });
validators> let activeSchema = infos[0].options.validator;
// 2. Query for any documents that do NOT satisfy the active schema
validators> db.users.find({ $nor: [ activeSchema ] })
// Empty Results.
There you have it. The two-phase resolution ensures that you can merge branches with different validators, and that all documents in the merged branch are valid according to the new validator.
What’s Next?#
Next week we’ll talk about collations, and the support added in version 0.4.1. We are currently deep in the design of branch-level permissions and push and pull support. Stay tuned!
If you are curious about Dumbo, Dolt, Doltgres or DoltLite, hop on our Discord and nerd out about version-controlled databases with us!