114

It seems mongo does not allow insertion of keys with a dot (.) or dollar sign ($) however when I imported a JSON file that contained a dot in it using the mongoimport tool it worked fine. The driver is complaining about trying to insert that element.

This is what the document looks like in the database:

{
    "_id": {
        "$oid": "..."
    },
    "make": "saab",
    "models": {
        "9.7x": [
            2007,
            2008,
            2009,
            2010
        ]
    }
}

Am I doing this all wrong and should not be using hash maps like that with external data (i.e. the models) or can I escape the dot somehow? Maybe I am thinking too much Javascript-like.

Community
  • 1
  • 1
Michael Yagudaev
  • 6,049
  • 3
  • 48
  • 53

25 Answers25

93

MongoDB doesn't support keys with a dot in them so you're going to have to preprocess your JSON file to remove/replace them before importing it or you'll be setting yourself up for all sorts of problems.

There isn't a standard workaround to this issue, the best approach is too dependent upon the specifics of the situation. But I'd avoid any key encoder/decoder approach if possible as you'll continue to pay the inconvenience of that in perpetuity, where a JSON restructure would presumably be a one-time cost.

JohnnyHK
  • 305,182
  • 66
  • 621
  • 471
  • Yeah I am aware of that, I read the documentation and the driver implementation for Node.js. I was just wondering what is the standard way to dealing with this. Because I would want to search based on those values too. Should I just build and encoder and decoder function to replace a `.` with `[dot]`? Or should I restructure my JSON somehow to avoid it all together? – Michael Yagudaev Sep 13 '12 at 00:41
  • 1
    I don't think there's a standard way, the best approach is too dependent upon the specifics of the situation. But I'd avoid any key encoder/decoder approach if possible as you'll continue to pay the inconvenience of that in perpetuity, where a JSON restructure would presumably be a one-time cost. – JohnnyHK Sep 13 '12 at 00:53
  • alright thanks for the advice, you should incorporate that into the answer and I will mark it as the answer to this question. – Michael Yagudaev Sep 13 '12 at 04:08
  • @yagudaev Okay, I added that into the answer. – JohnnyHK Sep 13 '12 at 04:30
  • 8
    Ran into this situation again. This seems to occur not so much with app key names, which we can control and often need to query on, but with user supplied data in nested data structures, which we can't control, but (a) would like to store in Mongo, (b) we know which specific fields this might happen in (e.g. `models` here), and (c) we don;t need query them by key name in Mongo. So a pattern that I settled on is to `JSON.stringify` this field on save, and 'JSON.parse` on retrieve. – prototype Mar 08 '13 at 17:59
  • 22
    If you must, you can provide {check_keys: false} option to bypass this issue. – Tzury Bar Yochay Apr 04 '13 at 06:20
  • 7
    @TzuryBarYochay OMG you've found the MongoDB equivalent of the northwest passage. I think this should be the accepted answer. – prototype Aug 03 '15 at 18:51
  • 1
    @TzuryBarYochay where do you add the {check_keys: false}? – emarel Jun 08 '16 at 17:56
  • 3
    @emarel db.collection_foo.update({this: "that"}, {$set: {a:"b"}}, {check_keys: false}) – Tzury Bar Yochay Jun 16 '16 at 19:03
  • I just encountered this issue this week. But, it wasn't mongo that was throwing the exception it was NodeJS. (Upon which MongoDb is built) JSON.parse is what throws the error. I feel like this is a fairly recent development, and wonder why NodeJS (or is it ES6) that does not like the period. In any case I just took using Array instead of "hash" tables built from JavaScript objects. – Jeffrey A. Gochin Jul 19 '16 at 23:38
  • Tzury Bar Yochay, can {check_keys: false} option be used in C# driver? I could not find anything... – PeterD Jun 13 '17 at 21:32
29

As mentioned in other answers MongoDB does not allow $ or . characters as map keys due to restrictions on field names. However, as mentioned in Dollar Sign Operator Escaping this restriction does not prevent you from inserting documents with such keys, it just prevents you from updating or querying them.

The problem of simply replacing . with [dot] or U+FF0E (as mentioned elsewhere on this page) is, what happens when the user legitimately wants to store the key [dot] or U+FF0E?

An approach that Fantom's afMorphia driver takes, is to use unicode escape sequences similar to that of Java, but ensuring the escape character is escaped first. In essence, the following string replacements are made (*):

\  -->  \\
$  -->  \u0024
.  -->  \u002e

A reverse replacement is made when map keys are subsequently read from MongoDB.

Or in Fantom code:

Str encodeKey(Str key) {
    return key.replace("\\", "\\\\").replace("\$", "\\u0024").replace(".", "\\u002e")
}

Str decodeKey(Str key) {
    return key.replace("\\u002e", ".").replace("\\u0024", "\$").replace("\\\\", "\\")
}

The only time a user needs to be aware of such conversions is when constructing queries for such keys.

Given it is common to store dotted.property.names in databases for configuration purposes I believe this approach is preferable to simply banning all such map keys.

(*) afMorphia actually performs full / proper unicode escaping rules as mentioned in Unicode escape syntax in Java but the described replacement sequence works just as well.

Steve Eynon
  • 4,979
  • 2
  • 30
  • 48
  • Should use ```//g``` to replace all occurrences and not just the first. Also, using the full-width equivalents as in Martin Konecny's answer seems to be a good idea. Finally, one backslash is enough for the encoding. ```key.replace(/\./g, '\uff0e').replace(/\$/g, '\uff04').replace(/\\/g, '\uff3c')``` – cw' Aug 04 '16 at 13:47
  • 1
    @cw' - The code is in a Java like syntax, so [replace](http://docs.oracle.com/javase/7/docs/api/java/lang/String.html#replace%28java.lang.CharSequence,%20java.lang.CharSequence%29) does actually replace all occurrences, and double backslashes are required for escaping backslashes. And again, you need to introduce some form of escaping to ensure *all* cases are covered. Someone, at some time, may actually want a a key of `U+FF04`. – Steve Eynon Aug 04 '16 at 15:25
  • 2
    As it turns out, Mongodb Supports dots and dollars in it's latest versions. See:- https://stackoverflow.com/a/57106679/3515086 – Abhidemon Jul 19 '19 at 06:26
  • 1
    Why \$ and not just $ though? – Moonlit Nov 19 '21 at 06:01
  • 1
    Hi @Moonlit, good question. It is because the example is written in [Fantom](https://fantom.org/forum/topic/) and the $ character is reserved for [String Interpolation](https://fantom.org/doc/docLang/Literals#interpolation) so it needs to be escaped with a backslash. So in effect, yes, IT IS just replacing "$". – Steve Eynon Nov 20 '21 at 09:25
20

The latest stable version (v3.6.1) of the MongoDB does support dots (.) in the keys or field names now.

Field names can contain dots (.) and dollar ($) characters now

h4ck3d
  • 6,134
  • 15
  • 51
  • 74
  • 13
    Even if the server supports it now, the driver still check for $ and dots in keys and do not accept them. Therefore Mongo only theoretically supports dots and dollar characters. Practically this is not yet usable :( – JMax Feb 28 '18 at 15:49
  • Maybe you're using some old or incompatible client. I've been using this on my production servers without any sweat. I've checked for NodeJS and Java clients. – h4ck3d Mar 22 '18 at 07:02
  • With Java it definetly does not work! Try following command: `mongoClient.getDatabase("mydb").getCollection("test").insertOne(new Document("value", new Document("key.with.dots", "value").append("$dollar", "value")));` It fails using mongodb-driver.3.6.3 and MongoDB 3.6.3. – JMax Mar 23 '18 at 09:33
  • 1
    Indeed, I just tried with a setup `mongodb-4.1.1` and `pymongo-3.7.1`. I can add documents containing keys with `.` with robomongo but not from `pymongo`, it sill raises `InvalidDocument: key '1.1' must not contain '.'` Wish it had been fixed by now... – Learning is a mess Jul 25 '18 at 11:29
  • I tried with mongodb server 4.0.9 and java driver 3.10.2 but it doesn't accept dot in key name. it's strange that when try that using robomongo it works... – xyzt Apr 28 '19 at 20:48
19

The Mongo docs suggest replacing illegal characters such as $ and . with their unicode equivalents.

In these situations, keys will need to substitute the reserved $ and . characters. Any character is sufficient, but consider using the Unicode full width equivalents: U+FF04 (i.e. “$”) and U+FF0E (i.e. “.”).

Tamlyn
  • 22,122
  • 12
  • 111
  • 127
Martin Konecny
  • 57,827
  • 19
  • 139
  • 159
  • 84
    That sounds like a recipe for massive debugging headaches down the road. – nobody Sep 19 '14 at 15:39
  • 2
    @AndrewMedico, @tamlyn - I think the docs mean something like `db.test.insert({"field\uff0ename": "test"})` – Myer Jul 29 '16 at 14:11
  • 5
    -1 A. That's a terrible idea - what if someone is actually trying to use those unicode characters as a key? Then you have a silent error that will do who knows what to your system. Don't use ambiguous escape methods like that. B. the mongo docs no longer say that, probably because someone realized its a terrible idea – B T Oct 28 '16 at 01:27
  • 1
    @BT: the docs still say that, just not at that page. https://docs.mongodb.com/v3.0/faq/developers/#dollar-sign-operator-escaping. But I agree, it's bad idea. :) – Sergio Tulentsev Nov 11 '16 at 08:51
  • 9
    @SergioTulentsev I got them to remove the recommendation : ) https://github.com/mongodb/docs/commit/3cb0c576256d27266a1ab5475dac715b746c157a – B T Nov 15 '16 at 03:19
  • 2
    @BT: hat tip to you, sir :) – Sergio Tulentsev Nov 15 '16 at 07:30
13

A solution I just implemented that I'm really happy with involves splitting the key name and value into two separate fields. This way, I can keep the characters exactly the same, and not worry about any of those parsing nightmares. The doc would look like:

{
    ...
    keyName: "domain.com",
    keyValue: "unregistered",
    ...
}

You can still query this easy enough, just by doing a find on the fields keyName and keyValue.

So instead of:

 db.collection.find({"domain.com":"unregistered"})

which wouldn't actually work as expected, you would run:

db.collection.find({keyName:"domain.com", keyValue:"unregistered"})

and it will return the expected document.

Steve
  • 1,480
  • 14
  • 22
10

You can try using a hash in the key instead of the value, and then store that value in the JSON value.

var crypto = require("crypto");   

function md5(value) {
    return crypto.createHash('md5').update( String(value) ).digest('hex');
}

var data = {
    "_id": {
        "$oid": "..."
    },
    "make": "saab",
    "models": {}
}

var version = "9.7x";

data.models[ md5(version) ] = {
    "version": version,
    "years" : [
        2007,
        2008,
        2009,
        2010
    ]
}

You would then access the models using the hash later.

var version = "9.7x";
collection.find( { _id : ...}, function(e, data ) {
    var models = data.models[ md5(version) ];
}
Henry
  • 2,870
  • 1
  • 25
  • 17
  • 1
    I like this, clean solution with 1-way hashing and really similar to the way things work under the hood. – Michael Yagudaev Apr 28 '14 at 19:08
  • 3
    The problem with using hashes as keys, is that they're not not guaranteed to be unique, and they frequently produce [collisions](http://crypto.stackexchange.com/questions/15873/what-is-the-md5-collision-with-the-smallest-input-values). Plus computing a cryptographic hash every time you want to access a map doesn't seem like the most optimal solution to me. – Steve Eynon Dec 28 '15 at 09:15
  • 2
    Why is this better than replacing the period with a special character or sequence? – B Seven Feb 23 '16 at 19:30
  • 1
    Converting strings to base64 is much better. – Zen Jul 18 '19 at 08:29
9

It is supported now

MongoDb 3.6 onwards supports both dots and dollar in field names. See below JIRA: https://jira.mongodb.org/browse/JAVA-2810

Upgrading your Mongodb to 3.6+ sounds like the best way to go.

Abhidemon
  • 540
  • 2
  • 7
  • 18
  • This is the best answer here. :+1 – hello_abhishek Jul 19 '19 at 09:18
  • 11
    3.6 can store them, yes, but it is *not* yet supported, may throw driver errors, and may break query/updates: [restrictions](https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names): "The MongoDB Query Language cannot always meaningfully express queries over documents whose field names contain these characters (see SERVER-30575). Until support is added in the query language, the use of $ and . in field names is not recommended and is **not supported** by the official MongoDB drivers." – JeremyDouglass Jul 26 '19 at 06:26
5

You'll need to escape the keys. Since it seems most people don't know how to properly escape strings, here's the steps:

  1. choose an escape character (best to choose a character that's rarely used). Eg. '~'
  2. To escape, first replace all instances of the escape character with some sequence prepended with your escape character (eg '~' -> '~t'), then replace whatever character or sequence you need to escape with some sequence prepended with your escape character. Eg. '.' -> '~p'
  3. To unescape, first remove the escape sequence from all instance of your second escape sequence (eg '~p' -> '.'), then transform your escape character sequence to a single escape character(eg '~s' -> '~')

Also, remember that mongo also doesn't allow keys to start with '$', so you have to do something similar there

Here's some code that does it:

// returns an escaped mongo key
exports.escape = function(key) {
  return key.replace(/~/g, '~s')
            .replace(/\./g, '~p')
            .replace(/^\$/g, '~d')
}

// returns an unescaped mongo key
exports.unescape = function(escapedKey) {
  return escapedKey.replace(/^~d/g, '$')
                   .replace(/~p/g, '.')
                   .replace(/~s/g, '~')
}
B T
  • 57,525
  • 34
  • 189
  • 207
  • This escaping can still break, if you got strings like '.~p.'. Here the escaped string will be '~p~~p~p'. Unescaping will give you '.~..', which is different from the actual string. – jvc Sep 18 '17 at 08:34
  • 1
    @jvc You're right! I've fixed the explanation and example escape functions. Let me know if they're still broken! – B T Sep 19 '17 at 02:20
4

From the MongoDB docs "the '.' character must not appear anywhere in the key name". It looks like you'll have to come up with an encoding scheme or do without.

maerics
  • 151,642
  • 46
  • 269
  • 291
3

A late answer, but if you use Spring and Mongo, Spring can manage the conversion for you with MappingMongoConverter. It's the solution by JohnnyHK but handled by Spring.

@Autowired
private MappingMongoConverter converter;

@PostConstruct
public void configureMongo() {
 converter.setMapKeyDotReplacement("xxx");
}

If your stored Json is :

{ "axxxb" : "value" }

Through Spring (MongoClient) it will be read as :

{ "a.b" : "value" }
PomPom
  • 1,468
  • 11
  • 20
3

As another user mentioned, encoding/decoding this can become problematic in the future, so it's probably just easier to replace all keys that have a dot. Here's a recursive function I made to replace keys with '.' occurrences:

def mongo_jsonify(dictionary):
    new_dict = {}
    if type(dictionary) is dict:
        for k, v in dictionary.items():
            new_k = k.replace('.', '-')
            if type(v) is dict:
                new_dict[new_k] = mongo_jsonify(v)
            elif type(v) is list:
                new_dict[new_k] = [mongo_jsonify(i) for i in v]
            else:
                new_dict[new_k] = dictionary[k]
        return new_dict
    else:
        return dictionary

if __name__ == '__main__':
    with open('path_to_json', "r") as input_file:
        d = json.load(input_file)
    d = mongo_jsonify(d)
    pprint(d)

You can modify this code to replace '$' too, as that is another character that mongo won't allow in a key.

1

I use the following escaping in JavaScript for each object key:

key.replace(/\\/g, '\\\\').replace(/^\$/, '\\$').replace(/\./g, '\\_')

What I like about it is that it replaces only $ at the beginning, and it does not use unicode characters which can be tricky to use in the console. _ is to me much more readable than an unicode character. It also does not replace one set of special characters ($, .) with another (unicode). But properly escapes with traditional \.

Mitar
  • 6,756
  • 5
  • 54
  • 86
1

Not perfect, but will work in most situations: replace the prohibited characters by something else. Since it's in keys, these new chars should be fairly rare.

/** This will replace \ with ⍀, ^$ with '₴' and dots with ⋅  to make the object compatible for mongoDB insert. 
Caveats:
    1. If you have any of ⍀, ₴ or ⋅ in your original documents, they will be converted to \$.upon decoding. 
    2. Recursive structures are always an issue. A cheap way to prevent a stack overflow is by limiting the number of levels. The default max level is 10.
 */
encodeMongoObj = function(o, level = 10) {
    var build = {}, key, newKey, value
    //if (typeof level === "undefined") level = 20     // default level if not provided
    for (key in o) {
        value = o[key]
        if (typeof value === "object") value = (level > 0) ? encodeMongoObj(value, level - 1) : null     // If this is an object, recurse if we can

        newKey = key.replace(/\\/g, '⍀').replace(/^\$/, '₴').replace(/\./g, '⋅')    // replace special chars prohibited in mongo keys
        build[newKey] = value
    }
    return build
}

/** This will decode an object encoded with the above function. We assume the structure is not recursive since it should come from Mongodb */
decodeMongoObj = function(o) {
    var build = {}, key, newKey, value
    for (key in o) {
        value = o[key]
        if (typeof value === "object") value = decodeMongoObj(value)     // If this is an object, recurse
        newKey = key.replace(/⍀/g, '\\').replace(/^₴/, '$').replace(/⋅/g, '.')    // replace special chars prohibited in mongo keys
        build[newKey] = value
    }
    return build
}

Here is a test:

var nastyObj = {
    "sub.obj" : {"$dollar\\backslash": "$\\.end$"}
}
nastyObj["$you.must.be.kidding"] = nastyObj     // make it recursive

var encoded = encodeMongoObj(nastyObj, 1)
console.log(encoded)
console.log( decodeMongoObj( encoded) )

and the results - note that the values are not modified:

{
  sub⋅obj: {
    ₴dollar⍀backslash: "$\\.end$"
  },
  ₴you⋅must⋅be⋅kidding: {
    sub⋅obj: null,
    ₴you⋅must⋅be⋅kidding: null
  }
}
[12:02:47.691] {
  "sub.obj": {
    $dollar\\backslash: "$\\.end$"
  },
  "$you.must.be.kidding": {
    "sub.obj": {},
    "$you.must.be.kidding": {}
  }
}
Nico
  • 4,248
  • 1
  • 20
  • 19
1

There is some ugly way to query it not recommended to use it in application rather than for debug purposes (works only on embedded objects):

db.getCollection('mycollection').aggregate([
    {$match: {mymapfield: {$type: "object" }}}, //filter objects with right field type
    {$project: {mymapfield: { $objectToArray: "$mymapfield" }}}, //"unwind" map to array of {k: key, v: value} objects
    {$match: {mymapfield: {k: "my.key.with.dot", v: "myvalue"}}} //query
])
sredni
  • 323
  • 2
  • 9
0

Lodash pairs will allow you to change

{ 'connect.sid': 's:hyeIzKRdD9aucCc5NceYw5zhHN5vpFOp.0OUaA6' }

into

[ [ 'connect.sid',
's:hyeIzKRdD9aucCc5NceYw5zhHN5vpFOp.0OUaA6' ] ]

using

var newObj = _.pairs(oldObj);
steampowered
  • 11,809
  • 12
  • 78
  • 98
0

You can store it as it is and convert to pretty after

I wrote this example on Livescript. You can use livescript.net website to eval it

test =
  field:
    field1: 1
    field2: 2
    field3: 5
    nested:
      more: 1
      moresdafasdf: 23423
  field3: 3



get-plain = (json, parent)->
  | typeof! json is \Object => json |> obj-to-pairs |> map -> get-plain it.1, [parent,it.0].filter(-> it?).join(\.)
  | _ => key: parent, value: json

test |> get-plain |> flatten |> map (-> [it.key, it.value]) |> pairs-to-obj

It will produce

{"field.field1":1,
 "field.field2":2,
 "field.field3":5,
 "field.nested.more":1,
 "field.nested.moresdafasdf":23423,
 "field3":3}
Andrey Stehno
  • 356
  • 1
  • 5
  • 9
0

Give you my tip: You can using JSON.stringify to save Object/ Array contains the key name has dots, then parse string to Object with JSON.parse to process when get data from database

Another workaround: Restructure your schema like:

key : {
"keyName": "a.b"
"value": [Array]
}
Mr.Cra
  • 1
  • 7
0

Latest MongoDB does support keys with a dot, but java MongoDB-driver is not supporting. So to make it work in Java, I pulled code from github repo of java-mongo-driver and made changes accordingly in their isValid Key function, created new jar out of it, using it now.

Rahul K Singh
  • 179
  • 10
0

Replace the dot(.) or dollar($) with other characters that will never used in the real document. And restore the dot(.) or dollar($) when retrieving the document. The strategy won't influence the data that user read.

You can select the character from all characters.

Cloud
  • 2,859
  • 2
  • 20
  • 23
0

The strange this is, using mongojs, I can create a document with a dot if I set the _id myself, however I cannot create a document when the _id is generated:

Does work:

db.testcollection.save({"_id": "testdocument", "dot.ted.": "value"}, (err, res) => {
    console.log(err, res);
});

Does not work:

db.testcollection.save({"dot.ted": "value"}, (err, res) => {
    console.log(err, res);
});

I first thought dat updating a document with a dot key also worked, but its identifying the dot as a subkey!

Seeing how mongojs handles the dot (subkey), I'm going to make sure my keys don't contain a dot.

Sam
  • 5,375
  • 2
  • 45
  • 54
0

Like what @JohnnyHK has mentioned, do remove punctuations or '.' from your keys because it will create much larger problems when your data starts to accumulate into a larger dataset. This will cause problems especially when you call aggregate operators like $merge which requires accessing and comparing keys which will throw an error. I have learnt it the hard way please don't repeat for those who are starting out.

Yi Xiang Chong
  • 744
  • 11
  • 9
0

For PHP I substitute the HTML value for the period. That's ".".

It stores in MongoDB like this:

  "validations" : {
     "4e25adbb1b0a55400e030000" : {
     "associate" : "true" 
    },
     "4e25adb11b0a55400e010000" : {
       "associate" : "true" 
     } 
   } 

and the PHP code...

  $entry = array('associate' => $associate);         
  $data = array( '$set' => array( 'validations.' . str_replace(".", `"."`, $validation) => $entry ));     
  $newstatus = $collection->update($key, $data, $options);      
CoolBeans
  • 20,654
  • 10
  • 86
  • 101
JRForbes
  • 9
  • 3
0

In our case the properties with the period is never queried by users directly. However, they can be created by users.

So we serialize our entire model first and string replace all instances of the specific fields. Our period fields can show up in many location and it is not predictable what the structure of the data is.

    var dataJson = serialize(dataObj);
    foreach(pf in periodFields) 
    {  
         var encodedPF = pf.replace(".", "ENCODE_DOT");
         dataJson.replace(pf, encodedPF);
    }

Then later after our data is flattened we replace instances of the encodedPF so we can write the decoded version in our files

Nobody will ever need a field named ENCODE_DOT so it will not be an issue in our case.

The result is the following color.one will be in the database as colorENCODE_DOTone

When we write our files we replace ENCODE_DOT with .

C Rudolph
  • 522
  • 7
  • 6
0

mongodb v5.0 start to support $setField, sample in below document: https://www.mongodb.com/docs/manual/reference/operator/aggregation/setField/

db.inventory.aggregate( [
   { $match: { _id: 1 } },
   { $replaceWith: {
        $setField: {
           field: "price.usd",
           input: "$$ROOT",
           value: 49.99
    } } }
] )
-3

/home/user/anaconda3/lib/python3.6/site-packages/pymongo/collection.py

Found it in error messages. If you use anaconda (find the correspondent file if not), simply change the value from check_keys = True to False in the file stated above. That'll work!

Xpleria
  • 5,472
  • 5
  • 52
  • 66
layang
  • 1