7

I got a document structured like the following. My question is how do I do the nested part "roles" validation on the database side. My requirements are:

  • the roles size could be 0 or more than 1.
  • the presence of name and created_by for a role if a role is created.

    {
      "_id": "123456",
      "name": "User Name",
      "roles": [
        {
          "name": "mobiles_user",
           "last_usage_at": {
             "$date": 1457000592991
            },
            "created_by": "987654",
            "created_at": {
              "$date": 1457000592991
            }
        },
        {
          "name": "webs_user",
           "last_usage_at": {
             "$date": 1457000592991
            },
            "created_by": "987654",
            "created_at": {
              "$date": 1457000592991
            }
        },
      ]
    }
    

At the moment, I am only doing the following for those none nested attributes:

db.createCollection( "users",
   { "validator" : {
     "_id" : {
         "$type" : "string"
      },
      "email" : {
         "$regex" : /@gmail\.com$/
      },
      "name" : {
         "$type" : "string"
      }
   }
} )

Could anyone please advise that how to do the nested document validation?

2
  • to clarify: you mean the size can be 0, 2, 3, etc. but not 1? Commented Mar 29, 2017 at 19:40
  • can you update the accepted answer? I don't want people coming here to be misled that this is impossible, I've demonstrated how it's possible. Commented Apr 4, 2017 at 6:37

2 Answers 2

9

Yes, you can validate all sub-documents in a document by negating $elemMatch, and you can ensure that the size is not 1. It's sure not pretty though! And not exactly obvious either.

> db.createCollection('users', {
...   validator: {
...     name: {$type: 'string'},
...     roles: {$exists: 'true'},
...     $nor: [
...       {roles: {$size: 1}},
...       {roles: {$elemMatch: {
...         $or: [
...           {name: {$not: {$type: 'string'}}},
...           {created_by: {$not: {$type: 'string'}}},
...         ]
...       }}}
...     ],
...   }  
... })
{ "ok" : 1 }

This is confusing, but it works! What it means is only accept documents where neither the size of roles is 1 nor roles has an element with a name that isn't a string or a created_by that isn't a string.

This is based upon the fact that in logic terms,

for all x: f(x) and g(x)

Is equivalent to

not exists x s.t.: not f(x) or not g(x)

We have to use the latter since MongoDB only gives us an exists operator.

Proof

Valid documents work:

> db.users.insert({
...   name: 'hello',
...   roles: [],
... })
WriteResult({ "nInserted" : 1 })

> db.users.insert({
...   name: 'hello',
...   roles: [
...     {name: 'foo', created_by: '2222'},
...     {name: 'bar', created_by: '3333'},
...   ]
... })
WriteResult({ "nInserted" : 1 })

If a field is missing from roles, it fails:

> db.users.insert({
...   name: 'hello',
...   roles: [
...     {name: 'foo', created_by: '2222'},
...     {created_by: '3333'},
...   ]
... })
WriteResult({
    "nInserted" : 0,
    "writeError" : {
        "code" : 121,
        "errmsg" : "Document failed validation"
    }
})

If a field in roles has the wrong type, it fails:

> db.users.insert({
...   name: 'hello',
...   roles: [
...     {name: 'foo', created_by: '2222'},
...     {name: 'bar', created_by: 3333},
...   ]
... })
WriteResult({
    "nInserted" : 0,
    "writeError" : {
        "code" : 121,
        "errmsg" : "Document failed validation"
    }
})

If roles has size 1 it fails:

> db.users.insert({
...   name: 'hello',
...   roles: [
...     {name: 'foo', created_by: '2222'},
...   ]
... })
WriteResult({
    "nInserted" : 0,
    "writeError" : {
        "code" : 121,
        "errmsg" : "Document failed validation"
    }
})

The only thing I can't figure out unfortunately is how to ensure that roles is an array. roles: {$type: 'array'} seems to fail everything, I presume because it's actually checking that the elements are of type 'array'?

Sign up to request clarification or add additional context in comments.

Comments

6

Edit: this answer is not correct, it is possible to validate all sub-documents in the array. See answer: https://stackoverflow.com/a/43102783/200224

You can't really. You can do things like:

"roles.name": { "$type": "string" }

But all that really means is at "at least one" of those properties need match the specified type. That means this would actually be valid:

{
    "_id" : "123456",
    "name" : "User Name",
    "roles" : [
            {
                    "name" : "mobiles_user",
                    "last_usage_at" : ISODate("2016-03-03T10:23:12.991Z"),
                    "created_by" : "987654",
                    "created_at" : ISODate("2016-03-03T10:23:12.991Z")
            },
            {
                    "name" : "webs_user",
                    "last_usage_at" : ISODate("2016-03-03T10:23:12.991Z"),
                    "created_by" : "987654",
                    "created_at" : ISODate("2016-03-03T10:23:12.991Z")
            },
            {
                    "name" : 1
            }
    ]
}

It is afterall "documement validation" and that is by nature not well suited to sub-documents in arrays, or any data in a contained array really.

The core of the implementation relies on expressions available to query operators, and since MongoDB lacks anythin in standard query expressions that equates to "All array entries must match this value" without being directly specific then it's not possible to express as a validator condition.

The only posibility to check array content like that in a "query" expression is using $where, and that is noted to not be an available option with document validation.

Even the $size operator available for queries must match a specific "size" value, and cannot use an in-equality condition. So you "could" verify a strict size, but not a minimal size, unless:

"roles.0": { "$exists": true }

This is a feature in "infancy" and somewhat experimental, so there is the possibility that future releases may address this.

But for now, your better option is to do such "schema validation" in client side code ( where you will get a lot better exception reporting ) instead. There are many libraries already existing that take that approach.

3 Comments

Thanks so much for such a descriptive answer. Yeah, you are right. The validation will indeed need to move to somewhere else, and since I am build this as a API, so I need to do this under my server side. Also, It would be great if MongoDB could provide a more descriptive response with where and what is failed.
@LPing It is a very new feature, and there also was a time when the official line was that MongoDB would "never" enforce schema validation. That has clearly changed, so there is the possibility that features to allow further validation might well be added somewhere along the line.
@BlakesSeven are you sure there isn't an ugly way to accomplish it using $not with $elemMatch?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.