# How to read a value at a nested path without null checks

> Read a deep value by a path held as a string, so a response mapper is a table of paths rather than a chain of optional accesses written out once per field.

Source: https://voxgig.com/howto/read-a-nested-value-by-path

- Audience: api-consumer
- Level: beginner
- Languages: typescript, javascript
- Verified: 2026-09-06
- Published: 2026-09-06

## Short answer

Use a path reader that takes the object and a dotted path, and returns undefined when any step is missing. Optional chaining does the same job when the path is known while you are writing the code. A path reader is for when the path is data: a mapping table, a configuration file, or a column a user picked.

---
## You will need

Node 22 or later, and a nested response you map onto your own shape. The example uses
[getpath from struct](https://github.com/voxgig/struct), which takes the object first and the path
second.

## Approaches compared

| Approach | When it fits | What it costs you | When to pick something else |
| --- | --- | --- | --- |
| [just-safe-get](https://github.com/angus-c/just) | You want one small function with no wider dependency | A package per operation, so a project doing several of these collects several | You already depend on a library that includes this |
| [lodash.get](https://lodash.com/docs/#get) | Any project, and the most widely recognized of the four | A dependency whose main package is large, though this entry point is not | Nothing in the project needs lodash otherwise |
| [Optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) | The path is known when you write the line | The path cannot come from data, so every field is a line of code | The path is configuration, or chosen at runtime |
| [struct getpath](https://github.com/voxgig/struct) | You already use struct for merging and transforming the same data | A smaller ecosystem, and its argument order differs from lodash | lodash is already in the project |

Voxgig maintains struct. It is one of four options here, not the recommendation.

The real split is between syntax and data. Optional chaining is syntax: it compiles to the same
guarded access you would write, and it costs nothing at runtime. The three functions take the path
as a value, which is what a mapping table needs, and they pay a small cost per read to parse it.

## Turn the mapper into a table

Once the path is data, the mapping is a table rather than a function body.

```ts title="read.mjs"
export function field(response, path, fallback = null) {
  const value = getpath(response, path)
  return value === undefined ? fallback : value
}

/** Read several paths at once, which is the usual shape of a response mapper. */
export function fields(response, paths) {
  return Object.fromEntries(Object.entries(paths).map(([name, path]) => [name, field(response, path)]))
}
```

The `fields` helper is the reason to bother. A mapping from your names to the vendor's is now an object literal a
reviewer reads in one go. Adding a field is a line of data rather than a line of code. That table can also come from a config file, which is what makes one mapper serve two vendors
without a branch in the code.

Substituting a fallback for undefined is a decision worth making once, centrally. A missing value and a value that is genuinely absent read the same way to the code
downstream. Choosing null rather than undefined means the result serializes to JSON with the key
present.

## See the three readers agree

The functions differ in ergonomics rather than in results, which is the useful thing to establish
before choosing between them.

```bash
node compare.mjs
```

```text output
customer.address.city      optional chaining: "Dublin"  lodash.get: "Dublin"  struct getpath: "Dublin"
customer.address.postcode  optional chaining: undefined  lodash.get: undefined  struct getpath: undefined
lines.0.amount             optional chaining: 2000  lodash.get: 2000  struct getpath: 2000
missing.deep.path          optional chaining: undefined  lodash.get: undefined  struct getpath: undefined
```

All three return the same value for every path here. That covers the array index and the path
that stops at its very first segment. The difference is in the source. The optional chaining column needs one branch per
path written out, and the other two take the path as an argument.

That equivalence is worth checking rather than assuming, because the libraries differ at the edges.
[lodash.get](https://lodash.com/docs/#get) accepts bracket notation such as `lines[0].amount` as well
as dots, and it substitutes a default you pass as a third argument. A reader written against one
convention and swapped for another returns undefined for the paths that used the old syntax, and
undefined is what a missing field looks like too.

## Check it worked

The cases that matter are the missing ones, because that is where a hand-written chain throws. The
fourth test maps three paths at once, which is the shape the mapper actually takes in production.

```bash
node --test read.test.mjs
```

```text output
1..4
# tests 4
# suites 0
# pass 4
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 129.541597
```

## When it goes wrong

The argument order is the first thing to get wrong. `getpath` takes the object first, and lodash
takes the object first, and several other libraries take the path first. Calling one with the other
order returns undefined rather than throwing, so the mapper produces nulls everywhere and the tests
that check a missing field still pass.

The second failure is a key containing a dot. A path is split on dots, so a response whose keys are
domain names or version numbers cannot be addressed with a dotted string. Pass the path as an array of segments, which every one of these functions accepts, and the
ambiguity disappears. That form is also faster, because nothing has to be split before the read.

The third is a path reader used where a schema belongs. Reading twelve paths tells you nothing about
whether the response was the shape you expected. A mapper full of nulls is what a changed API looks
like from the inside, and it is indistinguishable from a customer whose record is genuinely empty.

## When not to do this

Do not replace optional chaining with a function call where the path is a literal. The syntax is
clearer at the point of use, has no dependency, and the compiler checks the property names against
your types.

Do not reach for struct to get one function. The package earns its place when you also merge,
transform or walk the same data, and a single path read is better served by the syntax or by a
one-function package.

Do not use a fallback to hide a required field. A mapper returning null for a value your code needs
moves the failure from the boundary to wherever that null is finally used. Keep the fallback for
values that really are optional, and let a missing required field raise where the response is read.

Do not build paths from user input without restricting them. A path is a read into arbitrary
structure, and one taken from a query parameter lets a caller address parts of an object you never
meant to expose.

## Related how-tos

- [Merge nested config objects with predictable precedence](/howto/merge-nested-config-objects-with-precedence)

- [Write a validator whose schema looks like the data](/howto/write-a-schema-that-looks-like-the-data)

## Last verified

Verified 2026-09-06 against Node 22.22.2, @voxgig/struct 0.3.5 and lodash.get 4.4.2. Both output
blocks are what the preceding command printed.