@Calendee

Firebase Validation hasChildren() Pro Tip

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.

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 ;) .