How-to › Parse, validate and transform data

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.

Audience
API consumer
Level
beginner
Topic
Transform nested data
Languages
TypeScript and JavaScript
Verified

A mapper turns a vendor response into your own shape, and it is forty lines of response.customer && response.customer.address && response.customer.address.city. Optional chaining shortens each line and leaves forty of them. When the vendor adds a field, somebody writes line forty-one, and the mapping nobody can see as a whole grows again.

What you get

You will end up reading nested values by a path held as data, so a mapper becomes a table. This is for you if your field mapping is a wall of guarded property access.

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, which takes the object first and the path second.

Approaches compared

ApproachWhen it fitsWhat it costs youWhen to pick something else
just-safe-getYou want one small function with no wider dependencyA package per operation, so a project doing several of these collects severalYou already depend on a library that includes this
lodash.getAny project, and the most widely recognized of the fourA dependency whose main package is large, though this entry point is notNothing in the project needs lodash otherwise
Optional chainingThe path is known when you write the lineThe path cannot come from data, so every field is a line of codeThe path is configuration, or chosen at runtime
struct getpathYou already use struct for merging and transforming the same dataA smaller ecosystem, and its argument order differs from lodashlodash 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.

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.

node compare.mjs
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 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.

node --test read.test.mjs
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.

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.

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 22 languages, from one model.

Get the Voxgig dispatch

Short notes on building SDKs, CLIs, REPLs, and MCPs for API-first teams, plus the occasional Fireside episode pick.

By signing up you agree to our Terms and Conditions.