biya:~$ cat our-default-value-was-not-a-valid-value.md
Our default value was not a valid value
A field defaulted to null. The list of allowed values did not include null. Every new merchant signup failed at the last step.
New merchant signups were failing. Not slowly, not sometimes: a merchant filled in the form, submitted, and the whole thing fell over at the end.
The cause was two lines in a schema that had been sitting next to each other for a while, each perfectly sensible on its own.
Two reasonable decisions
An employee record has a station, the part of the kitchen they work at. It is optional, because plenty of staff are not assigned to one, so the field was given a default:
station: {
type: String,
enum: ['grill', 'fryer', 'salad', 'bar'],
default: null,
}Both halves are defensible. The enum stops a typo becoming a station nobody can filter by. The default says an unassigned employee has no station, which is exactly what null is for.
Put together they are a contradiction. The default is not one of the permitted values.
Why nothing caught it earlier
Defaults are applied before validation, not instead of it. The document is built, the default fills the empty field, and then the whole thing is checked against the rules. null is not in the enum, so the document is invalid the moment it is created, without anyone having touched that field.
That has a shape worth noticing: the failure needs no bad input. Nobody typed anything wrong. There is no request you could look at and see the problem in. The record was rejected for a value the system had supplied to itself.
It only bit on new signups because that is the path that creates employees from nothing. Existing merchants had employee records written before the enum was tightened, and nothing re-validated them. The bug was invisible until the exact moment a new customer tried to join, which is the least convenient moment available.
The fix
Delete the default.
station: {
type: String,
enum: ['grill', 'fryer', 'salad', 'bar'],
}An absent field is already the thing the default was trying to express. Setting it to null was a way of saying "nothing here" in a system that already had a way of saying that, and the second way was not on the list of things you are allowed to say.
What we took from it
A default is a value like any other, and it has to pass the same validation. It is easy to read a default as configuration, something that sits outside the rules and describes them. It does not. It is data the system writes on your behalf, and it is checked exactly as strictly as data a user writes.
When you tighten a constraint, check what already writes to that field. The enum was added later and was correct. What it missed was that the schema itself was one of the writers.
Absence and null are not the same claim, and picking both is a bug waiting for a validator. One says the question was never asked. The other says the question was asked and the answer was nothing. Storing the second where the first was meant only works until something starts checking.