1

Given an object or array, I want to be able to determine if the path exists or not.

Given - Example 1

const spath = "data/message";
const body = {
  data: {
    school: 'yaba',
    age: 'tolu',
    message: 'true'
  },
  time: 'UTC',
  class: 'Finals'
}

it should return true because message can be found in body.data.message else return false.

Given - Example 2

const spath = "data/message/details/lastGreeting";
const body = {
  data: {
    school: 'yaba',
    age: 'tolu',
    message: {
       content: 'now',
       details: {
          lastGreeting: true
       }
    }
  },
  time: 'UTC',
  class: 'Finals'
}

it should return true because lastGreeting can be found in body.data.message.details.lastGreeting else return false.

The other condition is when the body consists of an array

Given - Example 3

const spath = "data/area/NY";
const body = {
  data: {
    school: 'yaba',
    age: 'tolu',
    names : ['darious'],
    area: [{
       NY: true,
       BG: true
    ]]
    message: {
       content: 'now',
       details: {
          lastGreeting: true
       }
    }
  },
  time: 'UTC',
  class: 'Finals'
}

it should return true because NY can be found in body.data.area[0].NY else return false.

This is the solution I came up with

const findPathInObject = (data, path, n) => {
  console.log('entered')
  console.log(data, path)
  

  if(!data){
    return false
  }
  
  let spath = path.split('/');
  for(let i = 0; i<n; i++){
    
    let lastIndex = spath.length - 1;
    if(spath[i] in data && spath[i] === spath[lastIndex]){
      return true
    }
    
    const currentIndex = spath[i];
//     spath.splice(currentIndex, 1);
    return findPathInObject(data[spath[currentIndex]], spath[i+1], spath.length)
    
  }
  
  return false
}

console.log(findPathInObject(body, spath, 3))
4
  • what is the problem, you are facing? Commented May 2, 2021 at 15:53
  • The lastIndex gets changed because of spath[i+1] - I want to prevent that. The lastindex should always be the last item in the path. Also make provisions for object that has array in them Commented May 2, 2021 at 16:00
  • do you want a recursive solution? Commented May 2, 2021 at 16:07
  • Yea a recursive solution would work too Commented May 2, 2021 at 16:10

6 Answers 6

3

You could take some checks in advance and check if path is an empry string, then exit with true.

By having an array, you could exit early by checking the elements of the array with the actual path by omitting the indices.

For the final check of a key, you could check the existence of it and return the result of the recursove call with the rest path or return false, if the key is not in the object.

const
    findPathInObject = (data, path) => {
        if (!path) return true;
        if (!data || typeof data !== 'object') return false;
        if (Array.isArray(data)) return data.some(d => findPathInObject(d, path));

        const
            spath = path.split('/'),
            key = spath.shift();

        return key in data
            ? findPathInObject(data[key], spath.join('/'))
            : false;
    };

console.log(findPathInObject({ data: { school: 'yaba', age: 'tolu', message: 'true' }, time: 'UTC', class: 'Finals' }, "data/message", 3)); // true

console.log(findPathInObject({ data: { school: 'yaba', age: 'tolu', message: { content: 'now', details: { lastGreeting: true } } }, time: 'UTC', class: 'Finals' }, "data/message/details/lastGreeting", 3)); // true

console.log(findPathInObject({ data: { school: 'yaba', age: 'tolu', names: ['darious'], area: [{ NY: true, BG: true }], message: { content: 'now', details: { lastGreeting: true } } }, time: 'UTC', class: 'Finals' }, "data/area/NY", 3)); // true

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

3 Comments

Thanks, when i try to check for path that doesn't exist, it throws an error e.g using this path - data/message/jdjfjdkf throws an error "TypeError: Cannot use 'in' operator to search for 'jdjfjdkf' in true
you need a check for an object, too. please see edit.
Thanks - i think your solution covers for the array part as well
2

find

For this answer, I'm going to provide a tree with varying degrees of nesting of objects and arrays -

const tree =
  { data:
      { school: "yaba", age: "tolu", message: "foo" }
  , classes:
      [ { name: "math" }, { name: "science" } ]
  , deep:
      [ { example:
            [ { nested: "hello" }
            , { nested: "world" }
            ]
        }
      ]
  }

Generators are a fantastic fit for this type of problem. Starting with a generic find which yields all possible results for a particular path -

function find (data, path)
{ function* loop (t, [k, ...more])
  { if (t == null) return
    if (k == null) yield t
    else switch (t?.constructor)
    { case Object:
        yield *loop(t[k], more)
      break
      case Array:
        for (const v of t)
          yield *loop(v, [k, ...more])
        break
    }
  }
  return loop(data, path.split("/"))
}
Array.from(find(tree, "classes/name"))
Array.from(find(tree, "deep/example/nested"))
Array.from(find(tree, "x/y/z"))
[ "math", "science" ]
[ "hello", "world" ]
[]

find1

If you want a function that returns one (the first) result, we can easily write find1. This is particularly efficient because generators are pauseable/cancellable. After the first result is found, the generator will stop searching for additional results -

function find1 (data, path)
{ for (const result of find(data, path))
    return result
}
find1(tree, "data/school")
find1(tree, "classes")
find1(tree, "classes/name")
find1(tree, "deep/example/nested")
find1(tree, "x/y/z")
"yaba"
[ { name: "math" }, { name: "science" } ]
"math"
"hello"
undefined

exists

If you care to check whether a particular path exists, we can write exists as a simple specialisation of find1 -

const exists = (data, path) =>
  find1(data, path) !== undefined
exists(tree, "data/school")
exists(tree, "classes")
exists(tree, "deep/example/nested")
exists(tree, "x/y/z")
true
true
true
false

demo

Expand the snippet below to verify the results in your own browser -

function find (data, path)
{ function* loop (t, [k, ...more])
  { if (t == null) return
    if (k == null) yield t
    else switch (t?.constructor)
    { case Object:
        yield *loop(t[k], more)
      break
      case Array:
        for (const v of t)
          yield *loop(v, [k, ...more])
        break
    }
  }
  return loop(data, path.split("/"))
}

function find1 (data, path)
{ for (const result of find(data, path))
    return result
}

const tree =
  { data:
      { school: "yaba", age: "tolu", message: "foo" }
  , classes:
      [ { name: "math" }, { name: "science" } ]
  , deep:
      [ { example:
            [ { nested: "hello" }
            , { nested: "world" }
            ]
        }
      ]
  }

console.log("find1")
console.log(find1(tree, "data/school"))
console.log(find1(tree, "classes"))
console.log(find1(tree, "classes/name"))
console.log(find1(tree, "deep/example/nested"))
console.log(find1(tree, "x/y/z"))
console.log("find")
console.log(Array.from(find(tree, "classes/name")))
console.log(Array.from(find(tree, "deep/example/nested")))
console.log(Array.from(find(tree, "x/y/z")))

Comments

1

Strictly spoken, body.data.area[0].NY is not in the path of body, sorry. body.data.area is in the path. For the object without body.data.area as array here's a solution. If you want to include objects within arrays as part of an objects path, the solution will be more complex

const spath = "data/area/NY";
const spath2 = "data/message/details/lastGreeting";
const notPath = "data/message/details/firstGreeting";
const body = {
  data: {
    school: 'yaba',
    age: 'tolu',
    names : ['darious'],
    area: {
       NY: true,
       BG: true
    },
    message: {
       content: 'now',
       details: {
          lastGreeting: true
       }
    }
  },
  time: 'UTC',
  class: 'Finals'
};

console.log(`${spath} exists? ${ exists(body, spath) && `yep` || `nope`}`);
console.log(`${spath2} exists? ${ exists(body, spath2) && `yep` || `nope`}`);
console.log(`${notPath} exists? ${ exists(body, notPath) && `yep` || `nope`}`);

function exists(obj, path) {
  const pathIterable = path.split("/");
  while (pathIterable.length) {
    const current = pathIterable.shift();
    // no path left and exists: true
    if (pathIterable.length < 1 && current in obj) { return true; }
    // up to now exists, path continues: recurse
    if (current in obj) { return exists(obj[current], pathIterable.join("/")); }
  }
  // no solution found: false
  return false;
}

2 Comments

Thanks, body.data.area[0].NY is not actually in the path. I can work with this. Thanks again
0

You can check this solution. Will also check for array of objects.

const body = {
    data: {
        school: 'yaba',
        age: 'tolu',
        message: {
            content: 'now',
            details: {
                lastGreeting: true,
            },
        },
        area: [
            {
                NY: true,
                BG: true,
            },
        ],
    },
    time: 'UTC',
    class: 'Finals',
};
const spath1 = 'data/message';
const spath2 = 'data/message/details/lastGreeting';
const spath3 = 'data/area/NY';
const spath4 = 'data/area/NY/Test';

console.log(`${spath1}: `, isPathExists(body, spath1.split('/'), 0));
console.log(`${spath2}: `, isPathExists(body, spath2.split('/'), 0));
console.log(`${spath3}: `, isPathExists(body, spath3.split('/'), 0));
console.log(`${spath4}: `, isPathExists(body, spath4.split('/'), 0));

function isPathExists(data, pathArr, i) {
    const key = pathArr[i];

    if (Array.isArray(data)) {
        for (let value of data) {
            if (isObject(value)) return isPathExists(value, pathArr, i);
        }
    } else if (data.hasOwnProperty(key)) {
        if (key === pathArr[pathArr.length - 1]) return true;
        return isPathExists(data[key], pathArr, i + 1);
    } else return false;

    return true;
}
function isObject(a) {
    return !!a && a.constructor === Object;
}

Comments

0

With help of a colleague we eventually came up with something simple and easy to comprehend that really suits our needs. The answer with the yield implementation solves the problem but we were looking for something someone can read and easily understand in the codebase. We wanted to be able to check if the path exists in the object as well as get the value.

So we added a third param called returnValue - by default it will always return the value. If we don't want it to do that, we can set the value of the return value to false and the function will check if the path am looking exists, if it does, it will return true else return false

This is what we finally came up with

const find = (path, data) => {
    if (Array.isArray(data)) {
        data = data[0];
    }
    for (const item in data) {
        if (item === path) {
            return data[item];
        }
    }
    return null;
};

const findPath = (fullPath, fullData, returnValue = true) => {

    const pathArray = fullPath.split('/');

    let findResult = fullData;
    for (const pathItem of pathArray) {
        findResult = find(pathItem, findResult);
        if (!findResult) {
            if (!returnValue) return false;
            return null;
        }
    }

    if (!returnValue) return true;
    return findResult;
};



const body = {
  name: 'mike',
  email: 1,
  data: {
    school: [
      {
        testing: 123
      }
    ]
  }
}

console.log(findPath('data/school/testing', body))

3 Comments

Note that this only checks the first element of an array. While findPath ('data/school/testing', {data: {school: [{testing: 123}]}}) yields 123, findPath ('data/school/testing', {data: {school: [{noTesting: 345}, {testing: 123}]}}) yields null, even though there is a testing in the second element. If that is your requirement, fine. But if not, let me suggest that you will learn a lot by looking at the answer from @Thankyou.
Yea, it works for our requirements. Thanks for the feedback. I could improve it to accommodate that too for the purpose of learning. Pls who is @Thankyou?
User Thankyou wrote the accepted answer, and writes some of the most inspiring and informative answers I've seen anywhere on StackOverflow.
0

Here is a clean, iterative solution using object-scan

.as-console-wrapper {max-height: 100% !important; top: 0}
<script type="module">
import objectScan from 'https://cdn.jsdelivr.net/npm/[email protected]/lib/index.min.js';

const data1 = { data: { school: 'yaba', age: 'tolu', message: 'true' }, time: 'UTC', class: 'Finals' };
const data2 = { data: { school: 'yaba', age: 'tolu', message: { content: 'now', details: { lastGreeting: true } } }, time: 'UTC', class: 'Finals' };
const data3 = { data: { school: 'yaba', age: 'tolu', names: ['darious'], area: [{ NY: true, BG: true }], message: { content: 'now', details: { lastGreeting: true } } }, time: 'UTC', class: 'Finals' };

const path1 = 'data/message';
const path2 = 'data/message/details/lastGreeting';
const path3 = 'data/area/NY';

const exists = (data, n) => objectScan([n.replace(/\//g, '.')], {
  useArraySelector: false,
  rtn: 'bool',
  abort: true
})(data);

console.log(exists(data1, path1));
// => true

console.log(exists(data2, path2));
// => true

console.log(exists(data3, path3));
// => true
</script>

Disclaimer: I'm the author of object-scan

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.