So, I'm chugging along with testing data validation in Firebase. Everything was going okay with my tests. I could block invalid data and accept valid data.
tl;dr : If you use .hasChildren()
validation, you must include a validation rule for every property you are requiring.
Original Rules:
{
"rules": {
".write": false,
".read": false,
"accounts": {
"$accountsId": {
".write": "$accountsId === auth.uid && !data.exists() && newData.exists()",
".read": "$accountsId === auth.uid",
"familyName": {
".validate": "newData.isString() && newData.val().length > 1 && newData.val().length < 100"
},
"cc": {
".validate": "newData.isString() && newData.val().length === 2"
},
"mobile": {
".validate": "newData.isString() && newData.val().length > 10 && newData.val().length < 25"
}
}
}
}
}
Then, I started to ensure rules would block any properties that I was not expecting and make sure all expected properties were provided. So, I modified my rules to to ensure new records had all the required properties and no extras.
Here are the new rules:
{
"rules": {
".write": false,
".read": false,
"accounts": {
"$accountsId": {
".write": "$accountsId === auth.uid && !data.exists() && newData.exists()",
".read": "$accountsId === auth.uid",
".validate": "newData.hasChildren(['familyName', 'cc', 'mobile', 'email'])",
"$other": {
".validate": false
},
"familyName": {
".validate": "newData.isString() && newData.val().length > 1 && newData.val().length < 100"
},
"cc": {
".validate": "newData.isString() && newData.val().length === 2"
},
"mobile": {
".validate": "newData.isString() && newData.val().length > 10 && newData.val().length < 25"
}
}
}
}
}
When I ran my tests, everything went to pot. My valid data was no longer being accepted. Huh?
After going over and over the changes, I finally realized something. If you have a .hasChildren
validation, then ALL of those children must also have a validation rule.
In my example above, I was requiring ['familyName', 'cc', 'mobile', 'email']
but I had no validation rule for email
. So, my "valid" data was not validating.
To fix this, I had to make sure I had a validation rule for email.
UPDATE : As pointed out by @gsoltis, the rules for 'email' were matched by $other
which set it to false. Unless something else validates it, it remains false.
@calendee 'email' was matching $other, which is .validate: false. We can def improve messaging though! Try the simulator in your account too
— Greg Soltis (@gsoltis) August 29, 2014
Final rules:
{
"rules": {
".write": false,
".read": false,
"accounts": {
"$accountsId": {
".write": "$accountsId === auth.uid && !data.exists() && newData.exists()",
".read": "$accountsId === auth.uid",
".validate": "newData.hasChildren(['familyName', 'cc', 'mobile', 'email'])",
"$other": {
".validate": false
},
"familyName": {
".validate": "newData.isString() && newData.val().length > 1 && newData.val().length < 100"
},
"cc": {
".validate": "newData.isString() && newData.val().length === 2"
},
"mobile": {
".validate": "newData.isString() && newData.val().length > 10 && newData.val().length < 25"
},
"email": {
".validate": "newData.val() === auth.email"
}
}
}
}
}
Of course, all of this would have been easier to figure out if Firebase provided real validation errors info ;) .