How do you trim white spaces in both the keys and values in a JavaScript Object recursively?
I came across one issue in which I was trying to \"clean\" a user suppli
I think a generic map
function handles this well. It separates the deep object traversal and transformation from the particular action we wish to perform -
const identity = x =>
x
const map = (f = identity, x = null) =>
Array.isArray(x)
? x.map(v => map(f, v))
: Object(x) === x
? Object.fromEntries(Object.entries(x).map(([ k, v ]) => [ map(f, k), map(f, v) ]))
: f(x)
const dirty =
` { " a ": " one "
, " b": [ null, { "c ": 2, " d ": { "e": " three" }}, 4 ]
, " f": { " g" : [ " five", 6] }
, "h " : [[ [" seven ", 8 ], null, { " i": " nine " } ]]
, " keep space ": [ " betweeen words. only trim ends " ]
}
`
const result =
map
( x => String(x) === x ? x.trim() : x // x.trim() only if x is a String
, JSON.parse(dirty)
)
console.log(JSON.stringify(result))
// {"a":"one","b":[null,{"c":2,"d":{"e":"three"}},4],"f":{"g":["five",6]},"h":[[["seven",8],null,{"i":"nine"}]],"keep space":["betweeen words. only trim ends"]}
map
can be reused to easily apply a different transformation -
const result =
map
( x => String(x) === x ? x.trim().toUpperCase() : x
, JSON.parse(dirty)
)
console.log(JSON.stringify(result))
// {"A":"ONE","B":[null,{"C":2,"D":{"E":"THREE"}},4],"F":{"G":["FIVE",6]},"H":[[["SEVEN",8],null,{"I":"NINE"}]],"KEEP SPACE":["BETWEEEN WORDS. ONLY TRIM ENDS"]}
making map
practical
Thanks to Scott's comment, we add some ergonomics to map
. In this example, we write trim
as a function -
const trim = (dirty = "") =>
map
( k => k.trim().toUpperCase() // transform keys
, v => String(v) === v ? v.trim() : v // transform values
, JSON.parse(dirty) // init
)
That means map
must accept two functional arguments now -
const map = (fk = identity, fv = identity, x = null) =>
Array.isArray(x)
? x.map(v => map(fk, fv, v)) // recur into arrays
: Object(x) === x
? Object.fromEntries(
Object.entries(x).map(([ k, v ]) =>
[ fk(k) // call fk on keys
, map(fk, fv, v) // recur into objects
]
)
)
: fv(x) // call fv on values
Now we can see key transformation working as separated from value transformation. String values get a simple .trim
while keys get .trim()
and .toUpperCase()
-
console.log(JSON.stringify(trim(dirty)))
// {"A":"one","B":[null,{"C":2,"D":{"E":"three"}},4],"F":{"G":["five",6]},"H":[[["seven",8],null,{"I":"nine"}]],"KEEP SPACES":["betweeen words. only trim ends"]}
Expand the snippet below to verify the results in your own browser -
const identity = x =>
x
const map = (fk = identity, fv = identity, x = null) =>
Array.isArray(x)
? x.map(v => map(fk, fv, v))
: Object(x) === x
? Object.fromEntries(
Object.entries(x).map(([ k, v ]) =>
[ fk(k), map(fk, fv, v) ]
)
)
: fv(x)
const dirty =
` { " a ": " one "
, " b": [ null, { "c ": 2, " d ": { "e": " three" }}, 4 ]
, " f": { " g" : [ " five", 6] }
, "h " : [[ [" seven ", 8 ], null, { " i": " nine " } ]]
, " keep spaces ": [ " betweeen words. only trim ends " ]
}
`
const trim = (dirty = "") =>
map
( k => k.trim().toUpperCase()
, v => String(v) === v ? v.trim() : v
, JSON.parse(dirty)
)
console.log(JSON.stringify(trim(dirty)))
// {"A":"one","B":[null,{"C":2,"D":{"E":"three"}},4],"F":{"G":["five",6]},"H":[[["seven",8],null,{"I":"nine"}]],"KEEP SPACES":["betweeen words. only trim ends"]}
You can clean up the property names and attributes using Object.keys to get an array of the keys, then Array.prototype.reduce to iterate over the keys and create a new object with trimmed keys and values. The function needs to be recursive so that it also trims nested Objects and Arrays.
Note that it only deals with plain Arrays and Objects, if you want to deal with other types of object, the call to reduce needs to be more sophisticated to determine the type of object (e.g. a suitably clever version of new obj.constructor()).
function trimObj(obj) {
if (!Array.isArray(obj) && typeof obj != 'object') return obj;
return Object.keys(obj).reduce(function(acc, key) {
acc[key.trim()] = typeof obj[key] == 'string'? obj[key].trim() : trimObj(obj[key]);
return acc;
}, Array.isArray(obj)? []:{});
}