Convert a JavaScript string in dot notation into an object reference

Viewed 192148

Given a JavaScript object,

var obj = { a: { b: '1', c: '2' } }

and a string

"a.b"

how can I convert the string to dot notation so I can go

var val = obj.a.b

If the string was just 'a', I could use obj[a]. But this is more complex. I imagine there is some straightforward method, but it escapes me at present.

33 Answers

2021

You don't need to pull in another dependency every time you wish for new capabilities in your program. Modern JS is very capable and the optional-chaining operator ?. is now widely supported and makes this kind of task easy as heck.

With a single line of code we can write get that takes an input object, t and string path. It works for object and arrays of any nesting level -

const get = (t, path) =>
  path.split(".").reduce((r, k) => r?.[k], t)
  
const mydata =
  { a: { b: [ 0, { c: { d: [ "hello", "world" ] } } ] } }

console.log(get(mydata, "a.b.1.c.d.0"))
console.log(get(mydata, "a.b.1.c.d.1"))
console.log(get(mydata, "a.b.x.y.z"))

"hello"
"world"
undefined

You can use the library available at npm, which simplifies this process. https://www.npmjs.com/package/dot-object

 var dot = require('dot-object');

var obj = {
 some: {
   nested: {
     value: 'Hi there!'
   }
 }
};

var val = dot.pick('some.nested.value', obj);
console.log(val);

// Result: Hi there!

using Array Reduce function will get/set based on path provided.

i tested it with a.b.c and a.b.2.c {a:{b:[0,1,{c:7}]}} and its works for both getting key or mutating object to set value

cheerz

    function setOrGet(obj, path=[], newValue){
      const l = typeof path === 'string' ? path.split('.') : path;
      return l.reduce((carry,item, idx)=>{
       const leaf = carry[item];
       
       // is this last item in path ? cool lets set/get value
       if( l.length-idx===1)  { 
         // mutate object if newValue is set;
         carry[item] = newValue===undefined ? leaf : newValue;
         // return value if its a get/object if it was a set
         return newValue===undefined ? leaf : obj ;
       }
    
       carry[item] = leaf || {}; // mutate if key not an object;
       return carry[item]; // return object ref: to continue reduction;
      }, obj)
    }
    
   
    console.log(
     setOrGet({a: {b:1}},'a.b') === 1 ||
    'Test Case: Direct read failed'
    )
    
    console.log(
     setOrGet({a: {b:1}},'a.c',22).a.c===22 ||
    'Test Case: Direct set failed'
    )
    
    console.log(
     setOrGet({a: {b:[1,2]}},'a.b.1',22).a.b[1]===22 ||
    'Test Case: Direct set on array failed'
    )
    
    console.log(
     setOrGet({a: {b:{c: {e:1} }}},'a.b.c.e',22).a.b.c. e===22 ||
    'Test Case: deep get failed'
    )
    
    // failed !. Thats your homework :) 
    console.log(
     setOrGet({a: {b:{c: {e:[1,2,3,4,5]} }}},'a.b.c.e.3 ',22)
    )
    
    
    
    

my personal recommendation.

do not use such a thing unless there is no other way!

i saw many examples people use it for translations for example from json; so you see function like locale('app.homepage.welcome') . this is just bad. if you already have data in an object/json; and you know path.. then just use it directly example locale().app.homepage.welcome by changing you function to return object you get typesafe, with autocomplete, less prone to typo's ..

If you wish to convert any object that contains dot notation keys into an arrayed version of those keys you can use this.


This will convert something like

{
  name: 'Andy',
  brothers.0: 'Bob'
  brothers.1: 'Steve'
  brothers.2: 'Jack'
  sisters.0: 'Sally'
}

to

{
  name: 'Andy',
  brothers: ['Bob', 'Steve', 'Jack']
  sisters: ['Sally']
}

convertDotNotationToArray(objectWithDotNotation) {

    Object.entries(objectWithDotNotation).forEach(([key, val]) => {

      // Is the key of dot notation 
      if (key.includes('.')) {
        const [name, index] = key.split('.');

        // If you have not created an array version, create one 
        if (!objectWithDotNotation[name]) {
          objectWithDotNotation[name] = new Array();
        }

        // Save the value in the newly created array at the specific index 
        objectWithDotNotation[name][index] = val;
        // Delete the current dot notation key val
        delete objectWithDotNotation[key];
      }
    });

}

I used this code in my project

const getValue = (obj, arrPath) => (
  arrPath.reduce((x, y) => {
    if (y in x) return x[y]
    return {}
  }, obj)
)

Usage:

const obj = { id: { user: { local: 104 } } }
const path = [ 'id', 'user', 'local' ]
getValue(obj, path) // return 104

Using object-scan seems a bit overkill, but you can simply do

// const objectScan = require('object-scan');

const get = (obj, p) => objectScan([p], { abort: true, rtn: 'value' })(obj);

const obj = { a: { b: '1', c: '2' } };

console.log(get(obj, 'a.b'));
// => 1

console.log(get(obj, '*.c'));
// => 2
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan@13.7.1"></script>

Disclaimer: I'm the author of object-scan

There are a lot more advanced examples in the readme.

This is one of those cases, where you ask 10 developers and you get 10 answers.

Below is my [simplified] solution for OP, using dynamic programming.

The idea is that you would pass an existing DTO object that you wish to UPDATE. This makes the method most useful in the case where you have a form with several input elements having name attributes set with dot (fluent) syntax.

Example use:

<input type="text" name="person.contact.firstName" />

Code snippet:

const setFluently = (obj, path, value) => {
  if (typeof path === "string") {
    return setFluently(obj, path.split("."), value);
  }

  if (path.length <= 1) {
    obj[path[0]] = value;
    return obj;
  }

  const key = path[0];
  obj[key] = setFluently(obj[key] ? obj[key] : {}, path.slice(1), value);
  return obj;
};

const origObj = {
  a: {
    b: "1",
    c: "2"
  }
};

setFluently(origObj, "a.b", "3");
setFluently(origObj, "a.c", "4");

console.log(JSON.stringify(origObj, null, 3));

function at(obj, path, val = undefined) {
  // If path is an Array, 
  if (Array.isArray(path)) {
    // it returns the mapped array for each result of the path
    return path.map((path) => at(obj, path, val));
  }
  // Uniting several RegExps into one
  const rx = new RegExp(
    [
      /(?:^(?:\.\s*)?([_a-zA-Z][_a-zA-Z0-9]*))/,
      /(?:^\[\s*(\d+)\s*\])/,
      /(?:^\[\s*'([^']*(?:\\'[^']*)*)'\s*\])/,
      /(?:^\[\s*"([^"]*(?:\\"[^"]*)*)"\s*\])/,
      /(?:^\[\s*`([^`]*(?:\\`[^`]*)*)`\s*\])/,
    ]
      .map((r) => r.source)
      .join("|")
  );
  let rm;
  while (rm = rx.exec(path.trim())) {
    // Matched resource
    let [rf, rp] = rm.filter(Boolean);
    // If no one matches found,
    if (!rm[1] && !rm[2]) {
      // it will replace escape-chars
      rp = rp.replace(/\\(.)/g, "$1");
    }
    // If the new value is set,
    if ("undefined" != typeof val && path.length == rf.length) {
      // assign a value to the object property and return it
      return (obj[rp] = val);
    }
    // Going one step deeper
    obj = obj[rp];
    // Removing a step from the path
    path = path.substr(rf.length).trim();
  }
  if (path) {
    throw new SyntaxError();
  }
  return obj;
}

// Test object schema
let o = { a: { b: [ [ { c: { d: { '"e"': { f: { g: "xxx" } } } } } ] ] } };

// Print source object
console.log(JSON.stringify(o));

// Set value
console.log(at(o, '.a["b"][0][0].c[`d`]["\\"e\\""][\'f\']["g"]', "zzz"));

// Get value
console.log(at(o, '.a["b"][0][0].c[`d`]["\\"e\\""][\'f\']["g"]'));

// Print result object
console.log(JSON.stringify(o));

Use this function:

function dotToObject(data) {
  function index(parent, key, value) {
    const [mainKey, ...children] = key.split(".");
    parent[mainKey] = parent[mainKey] || {};

    if (children.length === 1) {
      parent[mainKey][children[0]] = value;
    } else {
      index(parent[mainKey], children.join("."), value);
    }
  }

  const result = Object.entries(data).reduce((acc, [key, value]) => {
    if (key.includes(".")) {
      index(acc, key, value);
    } else {
      acc[key] = value;
    }

    return acc;
  }, {});
  return result;
}

module.exports = { dotToObject };

Ex:

const user = {
  id: 1,
  name: 'My name',
  'address.zipCode': '123',
  'address.name': 'Some name',
  'address.something.id': 1,
}

const mappedUser = dotToObject(user)
console.log(JSON.stringify(mappedUser, null, 2))

Output:

{
  "id": 1,
  "name": "My name",
  "address": {
    "zipCode": "123",
    "name": "Some name",
    "something": {
      "id": 1
    }
  }
}

If you want to do this in the fastest possible way, while at the same time handling any issues with the path parsing or property resolution, check out path-value.

const {resolveValue} = require('path-value');

const value = resolveValue(obj, 'a.b.c');

The library is 100% TypeScript, works in NodeJS + all web browsers. And it is fully extendible, you can use lower-level resolvePath, and handle errors your own way, if you want.

const {resolvePath} = require('path-value');

const res = resolvePath(obj, 'a.b.c'); //=> low-level parsing result descriptor

Wanted to put this one out there:

function propertyByPath(object, path = '') {
    if (/[,(){}&|;]/.test(path)) {
        throw 'forbidden characters in path';
    }
    return Function(
      ...Object.keys(window).filter(k => window[k] instanceof Window || window[k] instanceof Document),
      "obj",
      `return ((o) => o${!path.startsWith('[') ? '.' : ''}${path})(...arguments, obj);`)
    .bind(object)(object);
}
propertyByPath({ a: { b: 'hello1' } }, "a.b"); // prints 'hello1'
propertyByPath({ a: { b: 'hello2' } }, "['a']?.b"); // returns 'hello2'
propertyByPath({ a: { b: 'hello2' } }, "a.b;console.log()"); // throws exception

The above code evaluates the path while doing an effort to prevent code execution while doing the path evaluation and masking Window and Document objects if somehow the execution prevention didn't succeeded.

I'm not saying it's 100% safe (though I'd love to see comments suggesting possible bypasses) nor efficient, but it might be suitable in cases where the flexibility of the path (optional chaining, etc.) along with some mitigations is needed.

Related