Skip to Content
  • Website
Codsen
  • Home
  • Open Source
  • Articles
  • About

prevOpen Source→object-merge-advancednext

object-merge-advanced14.2.4

Deeply merge JSON-like data structures

Downloads per monthChangelogMIT Licenselibera manifesto
  • the top
  • Installation
  • Quick Take
  • Examples
  • PURPOSE
  • IN PRACTICE
  • API — MERGEADVANCE…
  • API — DEFAULTS
  • API — VERSION
  • API — TYPES
  • OPTS — CB
  • OPTS — CB EXAMPLE #1
  • OPTS — CB EXAMPLE #2
  • OPTS — CB EXAMPLE #3
  • OPTS — MERGEOBJECTS…
  • DIFFERENCE FR…
  • DIFFERENCE FR…
  • Changelog

No 3rd party dependencies. All dependencies and devDependencies, checked recursively, are Codsen packages.

Permalink to InstallationInstallation

Permalink to Quick TakeQuick Take

Permalink to ExamplesExamples

  • Override a merge result in the callback
  • Deduplicate and sort merged string arrays
  • Clear an array merge when either array contains strings
  • Concatenate every clashing pair of arrays
  • Concatenate arrays on a selected key
  • Prefer incoming values for every clash
  • Take the incoming value for a selected clashing key
  • Keep existing values for every clash
  • Merge array values and omit duplicates instead of concatenating positions
  • Merge Boolean clashes with AND instead of the default OR
  • Merge positioned array objects even when their key sets differ
  • Let null explicitly override another value
  • Apply one array object to every object in the other array
  • Reuse caller-owned inputs instead of defensively cloning them

Purpose

It’s like Lodash _.merge, but it correctly merges different-type things and behaves well when it encounters nested things like parsed HTML (lots of nested arrays, objects and strings).

Imagine if the identical keys of two objects were merged by judging their values by the hierarchy instead:

  • non-empty array trumps all below
  • non-empty plain object trumps all below
  • non-empty string …
  • empty plain object …
  • empty array
  • empty string
  • number
  • boolean
  • null
  • undefined doesn’t trump anything

The idea is, we strive to retain as much datum as possible after merging. For example, you’d be better off with a non-empty string than with an empty array or boolean.

There are plenty of settings (mainly aimed at templating needs) but you can tap the callback and override the result in any way you like.

That’s what this library does

When object-merge-advanced merges two objects, it will recursively traverse each key and compare:

  • If a key exists only in one of the objects, it goes straight into the result object.
  • If a key exists on both, there’s a clash. Key’s value will be chosen judging by its value’s type:
    • Arrays trump objects which trump strings which trump numbers which trump Booleans
    • Non-empty array as value trumps any object or string as value
    • Anything empty won’t trump anything not empty
    • If both keys have plain object values, they’ll get recursively fed back into the library again
    • Booleans will be merged using logical “OR”
    • Arrays will be merged, and if there are objects within, those objects will be merged smartly, depending if their keysets are similar. If not, objects will be merged as separate array elements.

There are ten possible combinations: 10 types of first input (object #1) and ten types of second input (object #2): non-empty (full) object, empty object, non-empty array, empty array, non-empty string, empty string, number, boolean, undefined and null.

matching algorithm
matching algorithm

A large number in the centre of a square shows which value prevails.

In the diagram above, the squares show which value gets assigned to the merge result — the first object’s (marked 1, pink fields) or second one’s (marked 2, sky blue fields).

In some cases, the program performs custom actions:

  1. passing value objects back into the main function recursively (when both values are plain objects),
  2. when merging arrays, it pays extra attention to the options object (if present) and the contents of both arrays (taking special measures for objects within),
  3. Logical “OR” composition (when both values are Boolean).
  4. Not to mention, all the custom overrides you put in the callback when overriding the result.

Check test.js unit tests to see this library in action.

In practice

We use this library to merge humongous JSON files that house our templates’ data. Booleans must be overwritten by strings/objects/arrays, but only non-empty-ones. This library can do such merging.

Also, we use it in small cases where Object.assign is not suitable, for example, when filling missing keys in a plain object or doing other operations on objects coming from JSON files.

API — mergeAdvanced()

The main function mergeAdvanced() is imported like this:

It accepts up to three input arguments:

By default, the inputs are not mutated. Calling the function with only input1 returns a defensive clone. The reuseInputs performance option is the only exception; see its ownership contract below.

Input argumentTypeObligatoryDescription
input1
Type: Anything
Obligatory: yes
input1AnythingyesNormally an object literal, but an array, string, or any other supported value also works. It can be deeply nested.
input2
Type: Anything
Obligatory: no
input2AnythingnoThe second value to merge. When omitted, it is treated as undefined, so an object or array in input1 is cloned.
opts
Type: Plain object or null
Obligatory: no
optsPlain object or nullnoMerge options. null is equivalent to omitting the options object.

Matching cycles are supported. When both graphs repeat the same compatible input pair, the result reuses that pair instead of expanding it once per path; different input pairs remain separate. Own JSON keys such as __proto__ remain own enumerable data properties in the result.

The Optional Options Object has the following shape:

KeyValueDefaultDescription
cb
Default: null
cbFunctionnullAllows you to intervene on each of merging actions, right before the values are returned. It gives you both values and suggested return result in a callback arguments. See below.
mergeObjectsOnlyWhenKeysetMatches
Default: true
mergeObjectsOnlyWhenKeysetMatchesBooleantrueControls the merging of the objects within arrays. See dedicated chapter below.
ignoreKeys
Default: n/a
ignoreKeysString / Array of stringsn/aKeeps matching clashing keys from input1. Supports whole-string wildcard patterns and leading ! exclusions.
hardMergeKeys
Default: n/a
hardMergeKeysString / Array of stringsn/aMakes input2 overwrite matching clashing keys from input1. Supports whole-string wildcard patterns and leading ! exclusions.
mergeArraysContainingStringsToBeEmpty
Default: false
mergeArraysContainingStringsToBeEmptyBooleanfalseIf any arrays contain strings, resulting merged array will be empty IF this setting is set to true.
oneToManyArrayObjectMerge
Default: false
oneToManyArrayObjectMergeBooleanfalseIf one array has one object, but another array has many objects, when oneToManyArrayObjectMerge is true, each object from “many-objects” array will be merged with that one object from “one-object” array. Handy when setting defaults on JSON data structures.
hardMergeEverything
Default: false
hardMergeEverythingBooleanfalseIf there’s a clash of anywhere, second argument’s value will always overwrite first one’s. That’s a unidirectional merge.
ignoreEverything
Default: false
ignoreEverythingBooleanfalseIf there’s a clash of anywhere, first argument’s value will always overwrite the second one’s. That’s a unidirectional merge.
concatInsteadOfMerging
Default: true
concatInsteadOfMergingBooleantrueIf it’s true (default), when object keys clash and their values are arrays, when merging, concatenateopens in a new tab those arrays. If it’s false, array contents from the first argument object’s key will go intact into final result, but second array’s contents will be added into result only if they don’t exist in the first array.
dedupeStringsInArrayValues
Default: false
dedupeStringsInArrayValuesBooleanfalseWhen two merged values are both arrays, full of strings and only strings, this option allows to dedupe the resulting array of strings. Setting should be used in conjunction with concatInsteadOfMerging to really ensure than resulting string array contains only unique strings.
mergeBoolsUsingOrNotAnd
Default: true
mergeBoolsUsingOrNotAndBooleantrueWhen two values are Booleans, by default, result will be calculated using logical OR on them. If you switch this to false, merging will use logical AND. Former setting is handy when dealing with JSON content driving email templates, latter is handy when merging settingsopens in a new tab (“off”, false overrides default “on”, true).
useNullAsExplicitFalse
Default: false
useNullAsExplicitFalseBooleanfalseWhen set to true, null vs. anything (argument order doesn’t matter) will yield null. This is used in data structures as an explicit “false” to “turn off” incoming defaults for good without the need of extra values or wrapping with conditionals in templates.
hardArrayConcat
Default: false
hardArrayConcatBooleanfalseWhen set to true, an array vs. array merge will always result from a concat operation from the input1 parameter with input2, no matter which items are contained on those arrays.
hardArrayConcatKeys
Default: n/a
hardArrayConcatKeysString / Array of stringsn/aEnables hardArrayConcat for matching clashing keys. Supports whole-string wildcard patterns and leading ! exclusions.
reuseInputs
Default: false
reuseInputsBooleanfalseAllows a callback-free merge to reuse caller-owned input nodes for speed. The result can alias and mutate those inputs. Enable it only when the caller gives the merge exclusive ownership; see the ownership contract below.

The three key-selector options use array-includes-with-glob. Patterns match the complete key. * matches zero or more characters, and a leading ! excludes matching keys from every positive pattern in the same selector array. Escape a literal leading exclamation mark as \!. A selector can be one string or a dense array of strings; holes and non-string entries are rejected.

An option whose value is explicitly undefined uses its default, just as if that key had been omitted.

reuseInputs ownership contract

Leave reuseInputs at its false default for ordinary application data. The default merge returns an independent result and does not mutate either caller input.

Set reuseInputs: true only when the merge has exclusive ownership of both inputs and no other code will observe them afterward. The function may return an input object, write merged values into it, and preserve aliases or cycles; one mutation can therefore be visible through every alias to that node. When a callback or oneToManyArrayObjectMerge is active, the implementation keeps defensive cloning enabled even if reuseInputs is true.

Here are all defaults in one place for copying:

The function will return merged result whose type depends on the inputs (see the chart).

API — defaults

You can import defaults:

It's a plain object:

The main function calculates the options to be used by merging the options you passed with these defaults.

The exported defaults object and its empty selector arrays are frozen. Copy it before deriving a custom full options object.

API — version

You can import version:

API — types

This package is written in TypeScript and exports the following types:

TypeDescription
InputOpts
Type: InputOpts
InputOptsThe optional options accepted by mergeAdvanced(). Each key can be omitted or set to undefined.
Opts
Type: Opts
OptsThe fully resolved options shape, including every required default.
InfoObj
Type: InfoObj
InfoObjThe fourth argument handed to opts.cb: display and lossless paths, the current key, and a tuple naming the two clashing value types.
PathSegment
Type: PathSegment
PathSegmentOne lossless path segment: a string object key or a numeric array index.
argType
Type: argType
argTypeThe set of type names which appear in InfoObj.type — "date", "object", "array", "string", "number", "function", "bigint", "boolean", "symbol", "null" and "undefined".
import type {
  argType,
  InfoObj,
  InputOpts,
  Opts,
  PathSegment,
} from "object-merge-advanced";

opts.cb

You can name the arguments of your callback function any way you like, only the order matters.

Argument at positionNamePurpose
1st
1stinputArg1 (call it anyway you like; for example, same as in Array.forEach, name the variables as you wish)An isolated copy of the original, pre-merge clashing value from the first main input.
2nd
2ndinputArg2 (call it anyway you like — only its position in a row matters)An isolated copy of the original, pre-merge clashing value from the second main input.
3rd
3rdresultAboutToBeReturned (call it anyway you like)Exactly what this merge branch would return without a callback.
4th
4thinfoObj (same — variable’s name is arbitrary)The current key, display and lossless paths, and the actual type of each clashing value. See the table below.

Remember always to return either 3rd arg. resultAboutToBeReturned or something else because otherwise undefined will be written as a result of the particular merge.

Fourth argument, infoObj is a plain object and will contain keys:

infoObj keyTypePurpose
path
Type: String
pathStringA legacy dotted display path shared by both sides. It is "" at the root and can be ambiguous when an object key itself contains a dot.
pathSegments
Type: Read-only array of strings/numbers
pathSegmentsRead-only array of strings/numbersThe lossless programmatic path. Object keys remain strings and array indexes are numbers, so dotted, empty, and numeric-looking object keys remain distinguishable.
key
Type: String or null
keyString or nullThe current object key. It is null at the root; array traversal retains the containing object’s key when available.
type
Type: Tuple of two argType strings
typeTuple of two argType stringsThe actual types of the two values being merged at this callback location.

The callback operands are isolated from caller data. Mutating either of the first two arguments does not mutate the original inputs. For program logic, prefer pathSegments over the dotted path display string.

Callback allows you to intervene on each of merging actions, right before the values are returned. It gives you both values (first two arguments), suggested return result (3rd argument) and info object (4th argument) in a callback arguments. Whatever you return from your callback function is then written as a final value. If you don’t want to do anything, just return that third argument. But you can return something different.

Callback is very powerful — you could pretty much use it instead of all the options listed higher.

For example, opts.ignoreEverything would be the same as returning the first argument in the callback instead of third. You can name arguments (inputArg1 and others) any way you like, only their order matters.

mergeAdvanced(
  {
    ...
  },
  {
    ...
  },
  {
    cb: (inputArg1, inputArg2, resultAboutToBeReturned, infoObj) => {
      // whatever you return here gets written as the value of clashing keys:
      return inputArg1
    },
  },
)

Also, opts.hardMergeEverything setting would be the same as returning callback’s second argument in every case:

mergeAdvanced(
  {
    ...
  },
  {
    ...
  },
  {
    cb: (inputArg1, inputArg2, resultAboutToBeReturned, infoObj) => {
      // whatever you return here gets written as the value of clashing keys:
      return inputArg2
    },
  },
)

opts.cb example #1

For example, you want to hard-merge (meaning, when values clash, second argument’s value always prevails) only the Boolean values, keeping the normal merging algorithm the same for the rest of the types.

Use the callback, passing it in the options. Inside, check the types and, instead of the suggested result (third argument), return the second argument — value from the second argument:

const res = mergeAdvanced(
  {
    // input #1
    a: {
      b: true,
      c: false,
      d: true,
      e: false,
    },
    b: "test",
  },
  {
    // input #2
    a: {
      b: false,
      c: true,
      d: true,
      e: false,
    },
    b: "", // <---- checking to make sure this empty string will not be hard-merged over "b" from input #1
  },
  {
    cb: (inputArg1, inputArg2, resultAboutToBeReturned, infoObj) => {
      if (typeof inputArg1 === "boolean" && typeof inputArg2 === "boolean") {
        return inputArg2;
      }
      return resultAboutToBeReturned;
    },
  },
);
console.log(`res = ${JSON.stringify(res, null, 4)}`);
// result:
// {
//   a: {
//     b: false,
//     c: true,
//     d: true,
//     e: false,
//   },
//   b: 'test', // <---- notice how hard merging on Bools didn't affect string
// }

opts.cb example #2

Another example: you want to wrap the values of what was merged with double curly braces ({{ and }}), but only if they are strings. Kind of logical, if you consider Booleans, null or plain objects will be clashing when the algorithm traverses each and every nested value.

Easy:

const res = mergeAdvanced(
  {
    a: {
      b: "old value for b",
      c: "old value for c",
      d: "old value for c",
      e: "old value for d",
    },
    b: false,
  },
  {
    a: {
      b: "var1", // <--- in this case, it will be non-empty-string vs. non-empty-string
      c: "var2", //      clashes, where second input's string goes to the result.
      d: "var3",
      e: "var4",
    },
    b: null,
  },
  {
    cb: (inputArg1, inputArg2, resultAboutToBeReturned, infoObj) => {
      if (typeof resultAboutToBeReturned === "string") {
        return `{{ ${resultAboutToBeReturned} }}`; // <--- use template literals
      }
      return resultAboutToBeReturned;
    },
  },
);
console.log(`res = ${JSON.stringify(res, null, 4)}`);
// => {
//      a: {
//        b: '{{ var1 }}',
//        c: '{{ var2 }}',
//        d: '{{ var3 }}',
//        e: '{{ var4 }}',
//      },
//      b: false, // <-- notice Boolean was not touched
//    }

Whatever you return from the callback will be written as a result of a clash, so make sure you return either resultAboutToBeReturned (third argument in the callback), or something to substitute it. Otherwise, undefined will be written.

opts.cb example #3

Let’s say you want to perform a regular merge on two objects, except a certain key merges need to be concatenated.

let obj1 = {
  key: "a",
  x: "z",
};

let obj2 = {
  key: "b",
  x: "y",
};

You are fine with y from obj2 overwriting x BUT you want values a and b concatenated (into ab).

To illustrate the case, I’ll put the key deeper to show you how the paths work:

const res = mergeAdvanced(
  {
    x: {
      key: "a", // <------- concatenate this
      c: "c val 1",
      d: "d val 1",
      e: "e val 1",
    },
    z: {
      key: "z.key val 1",
    },
  },
  {
    x: {
      key: "b", // <------- with this, but only this path
      c: "c val 2",
      d: "d val 2",
      e: "e val 2",
    },
    z: {
      key: "z.key val 2", // <---- even though this key is also same-named
    },
  },
  {
    cb: (inputArg1, inputArg2, resultAboutToBeReturned, infoObj) => {
      if (
        infoObj.pathSegments.length === 2 &&
        infoObj.pathSegments[0] === "x" &&
        infoObj.pathSegments[1] === "key"
      ) {
        // here are all the contents of the "infoObj":
        console.log(`${`\u001b[${33}m${`infoObj`}\u001b[${39}m`} = ${JSON.stringify(infoObj, null, 4)}`);
        return `${typeof inputArg1 === "string" && inputArg1.length > 0 ? inputArg1 : ""}` + `${typeof inputArg2 === "string" && inputArg2.length > 0 ? inputArg2 : ""}`;
      }
      return resultAboutToBeReturned;
    },
  },
);
// ==> {
//       x: {
//         key: "ab",   // <---------------- concatenated
//         c: "c val 2",
//         d: "d val 2",
//         e: "e val 2"
//       },
//       z: {
//         key: "z.key val 2"
//       }
//     }

opts.mergeObjectsOnlyWhenKeysetMatches use cases

mergeObjectsOnlyWhenKeysetMatches is an extra insurance from accidental merging two objects within arrays, where key sets are too different (both have at least one unique key).

For example:

Let’s merge these two objects. Notice that each has a unique key (yyyy and xxxx in the object that sits within the first position of each array).

// #1
const obj1 = {
  a: [
    {
      a: "a",
      b: "b",
      yyyy: "yyyy",
    },
  ],
};

const obj2 = {
  a: [
    {
      xxxx: "xxxx",
      b: "b",
      c: "c",
    },
  ],
};

const res1 = mergeAdvanced(object1, object2);

console.log("res1 = " + JSON.stringify(res1, null, 4));
// => {
//      a: [
//        {
//          a: 'a',
//          b: 'b',
//          yyyy: 'yyyy'
//        },
//        {
//          xxxx: 'xxxx',
//          b: 'b',
//          c: 'c'
//        }
//      ]
//    }

but if you turn off the safeguard, { mergeObjectsOnlyWhenKeysetMatches: false } each object within an array is merged no matter their differences in the keysets:

const res2 = mergeAdvanced(object1, object2, {
  mergeObjectsOnlyWhenKeysetMatches: false,
});
console.log("res2 = " + JSON.stringify(res2, null, 4));
// => {
//      a: [
//        {
//          a: 'a',
//          b: 'b',
//          yyyy: 'yyyy',
//          xxxx: 'xxxx',
//          c: 'c'
//        }
//      ]
//    }

Difference from Lodash _.merge

Lodash _.mergeopens in a new tab gets stuck when encounters a mismatching type values within plain objects. It’s neither suitable for merging AST’s, nor for deep recursive merging.

Difference from Object.assign()

Object.assign() is just a hard overwrite of all existing keys, from one object to another. It does not weigh the types of the input values and will happily overwrite the string value with a boolean placeholder.

Object.assign() is not for merging data objects, it’s for setting defaults in the options objects.

For example, in our email template builds, we import SCSS variables file as an object. We also import variables for each template, and template variables object overwrites anything existing in SCSS variables object.

That’s because we want to be able to overwrite global colours per-template when needed.

Now imagine you’re merging those two objects, and SCSS variables object has a key "mainbgcolor": "#ffffff". Now, a vast majority of templates don’t need any customisation for the main background, therefore in their content JSON files the key is set to default, Boolean false: "mainbgcolor": false.

If merging were done using object-assign, placeholder false would overwrite real string value "#ffffff. That means, HTML would receive “false” as a CSS value, which is pink!

If merging were done using object-merge-advanced, all would be fine, because String trumps Boolean — placeholder falses would not overwrite the default SCSS string values.

Permalink to changelogChangelog

Open Changelog
↑ back to top
prev next

Copyright

All rights reserved © Roy Revelt 2026
All our open source packages are under MIT licenceopens in a new tab

Activities

🐛 See a bug? Raise an issueopens in a new tab
💘 Check out the Indiewebopens in a new tab and Libera manifestoopens in a new tab