Reputation: 1890
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 supplied JSON string and send it into my other code for further processing.
Let's say we've got a user supplied JSON string whose property key and value are of type "string". However, what's problematic in this case is that the keys and values are not as clean as desired. Say a { " key_with_leading_n_trailing_spaces ": " my_value_with_leading_spaces" }.
In this case, it can easily cause issue with your brilliantly written JavaScript program trying to make use of such data(or should we call it dirty data?) because when your code is trying to get the value out of this JSON object, not only the key is not matching but also the value can not be matched. I have looked around google and found a few tips but there is not one cure that cure it all.
Given this JSON with lots of white spaces in keys and values.
var badJson = {
" some-key ": " let it go ",
" mypuppy ": " donrio ",
" age ": " 12.3",
" children ": [
{
" color": " yellow",
"name ": " alice"
}, {
" color": " silver ",
"name ": " bruce"
}, {
" color": " brown ",
" name ": " francis"
}, {
" color": " red",
" name ": " york"
},
],
" house": [
{
" name": " mylovelyhouse ",
" address " : { "number" : 2343, "road " : " boardway", "city " : " Lexiton "}
}
]
};
So this is what I came up with ( with help of using lodash.js):
//I made this function to "recursively" hunt down keys that may
//contain leading and trailing white spaces
function trimKeys(targetObj) {
_.forEach(targetObj, function(value, key) {
if(_.isString(key)){
var newKey = key.trim();
if (newKey !== key) {
targetObj[newKey] = value;
delete targetObj[key];
}
if(_.isArray(targetObj[newKey]) || _.isObject(targetObj[newKey])){
trimKeys(targetObj[newKey]);
}
}else{
if(_.isArray(targetObj[key]) || _.isObject(targetObj[key])){
trimKeys(targetObj[key]);
}
}
});
}
//I stringify this is just to show it in a bad state
var badJson = JSON.stringify(badJson);
console.log(badJson);
//now it is partially fixed with value of string type trimed
badJson = JSON.parse(badJson,function(key,value){
if(typeof value === 'string'){
return value.trim();
}
return value;
});
trimKeys(badJson);
console.log(JSON.stringify(badJson));
Note here : I did this in a 1, 2 steps because I could not find a better one shot to deal it all solution. If there is issue in my code or anything better, please do share with us.
Thanks!
Upvotes: 17
Views: 37718
Reputation: 10864
The best solution I used is this. Check the documentation on replacer function.
function trimObject(obj){
var trimmed = JSON.stringify(obj, (key, value) => {
if (typeof value === 'string') {
return value.trim();
}
return value;
});
return JSON.parse(trimmed);
}
var obj = {"data": {"address": {"city": "\n \r New York", "country": " USA \n\n\r"}}};
console.log(trimObject(obj));
Upvotes: 5
Reputation: 11
@RobG Thank you for the solution. Adding one more condition will not create more nested objects
function trimObj(obj) {
if (obj === null && !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() : typeof obj[key] === 'object' ? trimObj(obj[key]) : obj[key];
return acc;
}, Array.isArray(obj)? []:{});
}
Upvotes: 1
Reputation: 135227
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"]}
Upvotes: 2
Reputation: 61
I tried the solution JSON.stringify solution above, but it will not work with a string like '"this is \'my\' test"'. You can get around it using stringify's replacer function and just trim the values going in.
JSON.parse(JSON.stringify(obj, (key, value) => (typeof value === 'string' ? value.trim() : value)))
Upvotes: 1
Reputation: 430
Similar to epascarello's answer. This is what I did :
import java.util.regex.Matcher;
import java.util.regex.Pattern;
........
public String trimWhiteSpaceAroundBoundary(String inputJson) {
String result;
final String regex = "\"\\s+|\\s+\"";
final Pattern pattern = Pattern.compile(regex);
final Matcher matcher = pattern.matcher(inputJson.trim());
// replacing the pattern twice to cover the edge case of extra white space around ','
result = pattern.matcher(matcher.replaceAll("\"")).replaceAll("\"");
return result;
}
Test cases
assertEquals("\"2\"", trimWhiteSpace("\" 2 \""));
assertEquals("2", trimWhiteSpace(" 2 "));
assertEquals("{ }", trimWhiteSpace(" { } "));
assertEquals("\"bob\"", trimWhiteSpace("\" bob \""));
assertEquals("[\"2\",\"bob\"]", trimWhiteSpace("[\" 2 \", \" bob \"]"));
assertEquals("{\"b\":\"bob\",\"c c\": 5,\"d\": true }",
trimWhiteSpace("{\"b \": \" bob \", \"c c\": 5, \"d\": true }"));
Upvotes: 1
Reputation: 16772
epascarello's answer above plus some unit tests (just for me to be sure):
function trimAllFieldsInObjectAndChildren(o: any) {
return JSON.parse(JSON.stringify(o).replace(/"\s+|\s+"/g, '"'));
}
import * as _ from 'lodash';
assert.true(_.isEqual(trimAllFieldsInObjectAndChildren(' bob '), 'bob'));
assert.true(_.isEqual(trimAllFieldsInObjectAndChildren('2 '), '2'));
assert.true(_.isEqual(trimAllFieldsInObjectAndChildren(['2 ', ' bob ']), ['2', 'bob']));
assert.true(_.isEqual(trimAllFieldsInObjectAndChildren({'b ': ' bob '}), {'b': 'bob'}));
assert.true(_.isEqual(trimAllFieldsInObjectAndChildren({'b ': ' bob ', 'c': 5, d: true }), {'b': 'bob', 'c': 5, d: true}));
assert.true(_.isEqual(trimAllFieldsInObjectAndChildren({'b ': ' bob ', 'c': {' d': 'alica c c '}}), {'b': 'bob', 'c': {'d': 'alica c c'}}));
assert.true(_.isEqual(trimAllFieldsInObjectAndChildren({'a ': ' bob ', 'b': {'c ': {'d': 'e '}}}), {'a': 'bob', 'b': {'c': {'d': 'e'}}}));
assert.true(_.isEqual(trimAllFieldsInObjectAndChildren({'a ': ' bob ', 'b': [{'c ': {'d': 'e '}}, {' f ': ' g ' }]}), {'a': 'bob', 'b': [{'c': {'d': 'e'}}, {'f': 'g' }]}));
Upvotes: 3
Reputation: 147403
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)? []:{});
}
Upvotes: 24
Reputation: 207511
You can just stringify it, string replace, and reparse it
JSON.parse(JSON.stringify(badJson).replace(/"\s+|\s+"/g,'"'))
Upvotes: 42