Compatibility
Published
There is a shape that turns up in every large codebase and almost never gets thought about as a contract: a property path written as a string. get(model, "user.profile.name"). A grid column declared as { key: "cart.total" }. A template that says {{user.profile.city}}. A JSON Pointer in a validation error. In all of them the resolver is your own code, and it is renamed consistently along with everything else -- but the path it walks is a string literal, and a string is not a rename site.
What was measured
One sample with a small path resolver of the kind every utility library ships: split the path on dots, walk the object one segment at a time with bracket access, return a fallback if a segment is missing. Around it, four things that are built on exactly that mechanism. A direct read of two paths, printed next to the same values read with dot access so the two can be compared in one line. A mustache-style renderer that substitutes placeholders from the same model. A table column list where each column carries a path string and a label. A writer that sets a value through a path, followed by a read-back with dot access. And a JSON-Pointer style lookup, to confirm the separator is not what matters.
The base column is clean on all five profiles: default target, modern target, gate profile, modern gate profile and string transforms. That last one is worth calling out, because the string transforms move literals into a table and encode them, and the paths still resolved correctly afterwards. Where a string is stored has no bearing on what it says.
The resolver survives; the path does not
With a pattern matching two of the model's property names, the dot-access read still printed the right value and the path-based read printed its fallback: get=Ada/42 became get=?/42. Both lines are in the same file, three lines apart, reading the same object. One is a member expression the transform rewrote; the other is a string the transform correctly left alone.
That is the whole mechanism, and it is worth stating in the direction that makes it predictable. The resolver's own code is fine. The object is fine. Every consistent, renamed member access in the file is fine. The only thing that is wrong is that one specific string still spells a name that no longer exists on the object, and no diagnostic anywhere is capable of noticing, because a lookup that misses is the same operation as a lookup for a property that was legitimately absent.
The JSON-Pointer arm confirms it is not about the dot. A path written with slashes and normalised before the walk failed identically, printing its fallback instead of the value. Any scheme that ends in bracket access with a string in hand behaves the same way.
The write that succeeds into the wrong place
The sharpest result is the writer. The sample sets a value through a path string and then reads the same location back with dot access, and the printed line went from after-set=Paris to after-set=London -- the value it held before the write.
Nothing failed. The write ran, created its intermediate objects if needed and stored the new value under the property the string names, which is the original name. The read-back went to the renamed property, which still holds the old value, and reported it. The object now carries both: the renamed one that the rest of the code reads, and the original one that the path writer keeps updating, drifting further apart with every write.
That is a nastier failure than the read case, because it does not present as missing data. It presents as an update that did not take effect -- a form that saves and re-opens with the old value, a setting that reverts, a cache that seems not to invalidate. Every one of those is normally investigated as a state-management or timing problem, and the actual cause is a string three files away.
What the user sees is the placeholder itself
The template arm is the one that reaches production visibly. The renderer resolved each placeholder through the same path lookup, and when the lookup missed it left the placeholder in place, so the rendered line went from Hi Ada, 42 in London to Hi {{user.profile.name}}, 42 in {{user.profile.city}}.
Whether the user sees raw braces or a blank space depends only on what your renderer does with a miss, and both are wrong in a way that is trivially visible in a screenshot and invisible in every automated check that does not assert on rendered text. The grid arm is the blank-space version of the same thing: the column with a renamed path printed its em-dash placeholder while the neighbouring column, whose path happened not to be matched, printed its value correctly. A half-populated table looks like a data problem, not a build problem.
The narrower arm makes the same point from the other side. A pattern matching only total left the name column correct and broke only the total column, both in the grid and in the template. The blast radius follows the pattern exactly, which means the symptom in a real build is usually partial: some fields resolve, some do not, and the ones that do not have no obvious property in common except that they matched a regular expression nobody was looking at.
The config object's own names are safe, the strings inside them are not
The clean arm draws the boundary precisely. A pattern matching key and label -- the field names of the column descriptor itself -- produced output identical to the original. Those names are written and read entirely inside the build, so both halves moved together.
So in a single small object, { key: "cart.total", label: "Total" }, the two property names are safe and the value of one of them is not. That is a useful thing to be able to say out loud when reviewing a rename pattern, because the instinct is to worry about the structure of configuration objects, and the structure is the part that takes care of itself.
It is also why this shape resists the usual advice slightly more than others. Anchoring a member pattern to your own application's names is the right general mitigation and it works here too -- but the names inside a path string are your own application's names. They are exactly the names a well-anchored pattern is designed to match. The gap is not that the pattern is too broad; it is that half the uses of those names are written as text.
Where this hides, and how to check
The shape is more common than it first appears. Utility helpers such as a get or set with a string path. Template engines that resolve dotted expressions at run time. Table and form libraries whose columns and fields are declared with path strings. Validation libraries that report errors as JSON Pointers and then use those pointers to attach messages to fields. Object-diff and deep-merge helpers that emit changed paths. Feature flags and analytics that read a value out of a model by a configured path. Anything that stores a path in a database or a JSON file so it can be changed without a deploy.
The last of those is the one to check first, because a path that lives outside the build cannot be updated when a rename pattern changes, and its failure will appear on a build where nothing about the path was touched at all.
The check is mechanical and does not need a running application. Search the codebase for string literals containing a dot between two identifier-shaped words, which finds the overwhelming majority of these paths, and compare that list against your member pattern. Anything that matches both is a defect waiting for the next release. For a build you already have, print a path-based read and a dot-access read of the same value next to each other in the protected artifact: if they disagree, this is what you are looking at, and no other cause produces that exact pair.
The mitigation, where the paths are yours to change, is to stop writing them as text -- an accessor function is renamed along with everything else and cannot drift. Where they are not yours to change, because a library or a stored configuration owns them, the names those paths reference belong in your reserved list.
Frequently asked questions
Does obfuscation break lodash get and similar path helpers?
Not in the default configuration. A path resolver, a template renderer, a column list, a path writer and a JSON-Pointer lookup all produced identical output across five protection profiles, including the string transforms that move literals into a table. The shape only becomes a surface when member renaming is switched on.
Why does a path lookup return its fallback while dot access works?
Because the transform rewrote the member expression and correctly left the string alone. The dot access now reads the renamed property, and the path string still spells the original name, which no longer exists on the object. In the measured arm the two forms printed different values three lines apart in the same file.
Why does my template show the placeholder instead of the value?
The renderer resolved the placeholder through a path lookup, the lookup missed, and the renderer left the placeholder text in place. The measured line rendered as Hi {{user.profile.name}} instead of the name. Whether a user sees raw braces or an empty space depends only on what your renderer does when a lookup misses.
Can a write through a path string go to the wrong property?
Yes, and it is the quietest result measured here. The write stored the value under the name the string spells, and the later dot-access read went to the renamed property and returned the value from before the write. The object ends up carrying both names, drifting further apart with every write, and the symptom looks like an update that did not take effect.
Are the field names of a column or field descriptor affected?
No. A pattern matching the descriptor's own key and label names produced output identical to the original, because those names are written and read entirely inside the build. In a single object the two property names are safe and the path string held as a value is not.
Does anchoring my member pattern to my own names fix this?
Only partly, and this shape is the exception worth knowing. The names inside a path string are your own application's names, so they are exactly what a well-anchored pattern is designed to match. The gap is not that the pattern is too broad, it is that half the uses of those names are written as text rather than as code.
How do I find these before shipping?
Search for string literals containing a dot between two identifier-shaped words and compare that list against your member pattern; anything matching both is a defect waiting for a release. Check paths stored outside the build first, since they cannot be updated when the pattern changes. On an existing build, print a path-based read and a dot-access read of the same value next to each other.
Related reading