How Do I Use The Replacer Function With JSON Stringify? - javascript

I've been trying to use
const fs = require("fs");
const settings = require("./serversettings.json")
let reason = args.join(' ');
function replacer(key, value) {
return reason;
}
fs.writeFileSync(settings, JSON.stringify(settings.logchannel, replacer))
It seems like to me that it doesn't work, so I'm trying to figure out how replacers work as MDN made me even more confused.

The replacer function takes a key and a value (as it passes through the object and its sub objects) and is expected to return a new value (of type string) that will replace the original value. If undefined is returned then the whole key-value pair is omitted in the resulting string.
Examples:
Log all key-value pairs passed to the replacer function:
var obj = {
"a": "textA",
"sub": {
"b": "textB"
}
};
var logNum = 1;
function replacer(key, value) {
console.log("--------------------------");
console.log("Log number: #" + logNum++);
console.log("Key: " + key);
console.log("Value:", value);
return value; // return the value as it is so we won't interupt JSON.stringify
}
JSON.stringify(obj, replacer);
Add the string " - altered" to all string values:
var obj = {
"a": "textA",
"sub": {
"b": "textB"
}
};
function replacer(key, value) {
if(typeof value === "string") // if the value of type string
return value + " - altered"; // then append " - altered" to it
return value; // otherwise leave it as it is
}
console.log(JSON.stringify(obj, replacer, 4));
Omitt all values of type number:
var obj = {
"a": "textA",
"age": 15,
"sub": {
"b": "textB",
"age": 25
}
};
function replacer(key, value) {
if(typeof value === "number") // if the type of this value is number
return undefined; // then return undefined so JSON.stringify will omitt it
return value; // otherwise return the value as it is
}
console.log(JSON.stringify(obj, replacer, 4));

settings is an object, not a filename.
I'm trying to replace the string in the settings named as "logchannel" to whatever I tell it to change it to
const fs = require("fs");
var settings = require("./serversettings.json");
settings.logchannel = "foo"; //specify value of logchannel here
fs.writeFileSync("./serversettings.json", JSON.stringify(settings));

Related

JSON.stringify replacer - how to get full path

Replacer in below code write on console current processed field name
let a = { a1: 1, a2:1 }
let b = { b1: 2, b2: [1,a] }
let c = { c1: 3, c2: b }
let s = JSON.stringify(c, function (field,value) {
console.log(field); // full path... ???
return value;
});
However I would like to get full "path" to field (not only its name) inside replacer function - something like this
c1
c2
c2.b1
c2.b2
c2.b2[0]
c2.b2[1]
c2.b2[1].a1
c2.b2[1].a2
How to do it?
Decorator
replacerWithPath in snippet determine path using this (thanks #Andreas for this tip ), field and value and some historical data stored during execution (and this solution support arrays)
JSON.stringify(c, replacerWithPath(function(field,value,path) {
console.log(path,'=',value);
return value;
}));
function replacerWithPath(replacer) {
let m = new Map();
return function(field, value) {
let path= m.get(this) + (Array.isArray(this) ? `[${field}]` : '.' + field);
if (value===Object(value)) m.set(value, path);
return replacer.call(this, field, value, path.replace(/undefined\.\.?/,''))
}
}
// Explanation fo replacerWithPath decorator:
// > 'this' inside 'return function' point to field parent object
// (JSON.stringify execute replacer like that)
// > 'path' contains path to current field based on parent ('this') path
// previously saved in Map
// > during path generation we check is parent ('this') array or object
// and chose: "[field]" or ".field"
// > in Map we store current 'path' for given 'field' only if it
// is obj or arr in this way path to each parent is stored in Map.
// We don't need to store path to simple types (number, bool, str,...)
// because they never will have children
// > value===Object(value) -> is true if value is object or array
// (more: https://stackoverflow.com/a/22482737/860099)
// > path for main object parent is set as 'undefined.' so we cut out that
// prefix at the end ad call replacer with that path
// ----------------
// TEST
// ----------------
let a = { a1: 1, a2: 1 };
let b = { b1: 2, b2: [1, a] };
let c = { c1: 3, c2: b };
let s = JSON.stringify(c, replacerWithPath(function(field, value, path) {
// "this" has same value as in replacer without decoration
console.log(path);
return value;
}));
BONUS: I use this approach to stringify objects with circular references here
There's just not enough information available in the replacer. These two objects have different shapes but produce the same sequence of calls:
let c1 = { c1: 3, c2: 2 };
let c2 = { c1: { c2: 3 } };
const replacer = function (field, value) {
console.log(field); // full path... ???
return value;
};
JSON.stringify(c1, replacer);
// logs c1, c2
JSON.stringify(c2, replacer);
// logs c1, c2
You'll have to write something yourself.
You can use custom walk function inside your replacer. Here's an example using a generator walk function:
const testObject = {a: 1, b: {a: 11, b: {a: 111, b: 222, c: 333}}, c: 3};
function* walk(object, path = []) {
for (const [key, value] of Object.entries(object)) {
yield path.concat(key);
if (typeof value === 'object') yield* walk(value, path.concat(key));
}
}
const keyGenerator = walk(testObject);
JSON.stringify(testObject, (key, value) => {
const fullKey = key === '' ? [] : keyGenerator.next().value;
// fullKey contains an array with entire key path
console.log(fullKey, value);
return value;
});
Console output:
fullKey | value
-------------------|------------------------------------------------------------
[] | {"a":1,"b":{"a":11,"b":{"a":111,"b":222,"c":333}},"c":3}
["a"] | 1
["b"] | {"a":11,"b":{"a":111,"b":222,"c":333}}
["b", "a"] | 11
["b", "b"] | {"a":111,"b":222,"c":333}
["b", "b", "a"] | 111
["b", "b", "b"] | 222
["b", "b", "c"] | 333
["c"] | 3
This works, assuming the algorithm in replacer is depth-first and consistent across all browsers.
Something like that. You need to adjust it for arrays. I think that you can do it yourself. The idea is clear.
let a = { a1: 1, a2:1 }
let b = { b1: 2, b2: [1,a] }
let c = { c1: 3, c2: b }
function iterate(obj, path = '') {
for (var property in obj) {
if (obj.hasOwnProperty(property)) {
if (typeof obj[property] == "object") {
iterate(obj[property], path + property + '.');
}
else {
console.log(path + property);
}
}
}
}
iterate(c)
Based on the other answers I have this function which adds a third path argument to the call of replacer:
function replacerWithPath(replacer) {
const m = new Map();
return function (field, value) {
const pathname = m.get(this);
let path;
if (pathname) {
const suffix = Array.isArray(this) ? `[${field}]` : `.${field}`;
path = pathname + suffix;
} else {
path = field;
}
if (value === Object(value)) {
m.set(value, path);
}
return replacer.call(this, field, value, path);
}
}
// Usage
function replacer(name, data, path) {
// ...
}
const dataStr = JSON.stringify(data, replacerWithPath(replacer));
BONUS:
I also created this function to iterate through an object in depth and to be able to use the replace function like with JSON.stringify.
The third argument to true will keep undefined values and empty objects.
It can be handy to modify and ignore values while iterating through an object, it returns the new object (without stringify).
function walkWith(obj, fn, preserveUndefined) {
const walk = objPart => {
if (objPart === undefined) {
return;
}
let result;
// TODO other types than object
for (const key in objPart) {
const val = objPart[key];
let modified;
if (val === Object(val)) {
modified = walk(fn.call(objPart, key, val));
} else {
modified = fn.call(objPart, key, val);
}
if (preserveUndefined || modified !== undefined) {
if (result === undefined) {
result = {};
}
result[key] = modified;
}
}
return result;
};
return walk(fn.call({ '': obj }, '', obj));
}
BONUS 2:
I use it to transform a data object coming from a form submission and containing files / arrays of files in mixed multipart, files + JSON.
function toMixedMultipart(data, bodyKey = 'data', form = new FormData()) {
const replacer = (name, value, path) => {
// Simple Blob
if (value instanceof Blob) {
form.append(path, value);
return undefined;
}
// Array of Blobs
if (Array.isArray(value) && value.every(v => (v instanceof Blob))) {
value.forEach((v, i) => {
form.append(`${path}[${i}]`, v);
});
return undefined;
}
return value;
};
const dataStr = JSON.stringify(data, replacerWithPath(replacer));
const dataBlob = new Blob([dataStr], { type: 'application/json' });
form.append(bodyKey, dataBlob);
return form;
}

Shorthand to manipulate an object attribute and reassign

I have a deeply nested object and I want to manipulate a value of it and reassign it again. Is there a shorthand way for this other than writing it all out again or assigning it to a variable:
createStops[idx]['place']['create'][stop][key][value] = createStops[idx]['place']['create'][stop][key][value].toString()
looks ugly doesn't it? Something like:
createStops[idx]['place']['create'][stop][key][value].toStringAndReassign()
but JS built in.
Edit: In my case it is a number, if it's for your case too please check out #MarkMeyer answer.
No, there isn't.
Assigning a new value requires an assignment.
Strings are immutable, so you can't convert an existing value into a string in-place.
Given a value that's a number, if you just want it to be a string, you can coerce to a string with an assignment operator:
let o = {
akey: {
akey:{
value: 15
}
}
}
o.akey.akey.value += ''
console.log(o)
No,
Going to the same index is needed to store the value
Although it is not possible as mentioned by #Quentin you can define a custom getter in your object like:
var foo = {
a: 5,
b: 6,
get c () {
return this.b.toString()+' text'
}
};
console.log(foo.c);
You're not reassigning the value as you are semantically formatting your values. In order to format your value you are mutating your initial object. If you do not pretend to modify an object for formatting purposes that will work just fine.
You do not have integrated functions to use like that, but you could use of some utilitary functions of your own to help you manage assignements and make it less verbal.
SPOIL : The final use look like
// call the function to do +1 at the specified key
executeFunctionAtKey(
// The object to change the value on
createStops,
// The path
`${idx}.place.create.${stop}.${key}.${value}`,
// The thing to do
(x) => x + 1,
);
const createStops = {
idx: {
place: {
create: {
stop: {
key: {
value: 5,
},
},
},
},
},
};
const idx = 'idx';
const stop = 'stop';
const key = 'key';
const value = 'value';
// Function that go to the specified key and
// execute a function on it.
// The new value is the result of the func
// You can do your toString there, or anything else
function executeFunctionAtKey(obj, path, func) {
const keys = path.split('.');
if (keys.length === 1) {
obj[path] = func(obj[key]);
return obj;
}
const lastPtr = keys.slice(0, keys.length - 1).reduce((tmp, x) => tmp[x], obj);
lastPtr[keys[keys.length - 1]] = func(lastPtr[keys[keys.length - 1]]);
return obj;
}
// call the function to do +1 at the specified key
executeFunctionAtKey(
// The object to change the value on
createStops,
// The path
`${idx}.place.create.${stop}.${key}.${value}`,
// The thing to do
(x) => x + 1,
);
console.log(createStops);
with the toString example from Number to String
const createStops = {
idx: {
place: {
create: {
stop: {
key: {
value: 5,
},
},
},
},
},
};
const idx = 'idx';
const stop = 'stop';
const key = 'key';
const value = 'value';
// Function that go to the specified key and
// execute a function on it.
// The new value is the result of the func
// You can do your toString there, or anything else
function executeFunctionAtKey(obj, path, func) {
const keys = path.split('.');
if (keys.length === 1) {
obj[path] = func(obj[key]);
return obj;
}
const lastPtr = keys.slice(0, keys.length - 1).reduce((tmp, x) => tmp[x], obj);
lastPtr[keys[keys.length - 1]] = func(lastPtr[keys[keys.length - 1]]);
return obj;
}
// call the function to do +1 at the specified key
executeFunctionAtKey(
// The object to change the value on
createStops,
// The path
`${idx}.place.create.${stop}.${key}.${value}`,
// The thing to do
(x) => x.toString(),
);
console.log(createStops);
Theoretically you could build a function that takes an object, a path and the property to set it to.
This will reduce the readability of your code, so i would advice using ordinary assignment. But if you need it check out the snippet below:
//
function setProp(object, path, val) {
var parts = path.split("/").filter(function (p) { return p.length > 0; });
var pathIndex = 0;
var currentTarget = object;
while (pathIndex < parts.length - 1) {
currentTarget = currentTarget[parts[pathIndex]];
pathIndex++;
}
if (val instanceof Function) {
currentTarget[parts[pathIndex]] = val(currentTarget[parts[pathIndex]]);
}
else {
currentTarget[parts[pathIndex]] = val;
}
return object;
}
var createStops = {
idx: {
place: {
create: {
stop: {
key: {
value: 5
}
}
}
}
}
};
function toString(p) { return p.toString(); }
console.log(JSON.stringify(createStops, null, 4));
setProp(createStops, 'idx/place/create/stop/key/value', toString);
console.log(JSON.stringify(createStops, null, 4));
UPDATE 1
Allowed passing functions and used OP JSON structure for snippet

get key name and values of a nested JSON object if element is not object

I have an object whose keys I don't know but structure is basically the same. Value can be a string or another object of strings/objects. Here is an example:
d = {
"name": "Sam",
"grade": 9,
"classes": {
"a": 1,
"b": 2
},
"age": null
}
What I want now is if value is not another object, get the key name and its value. If value is null, return empty string. From the above the expected output is:
name=Sam, grade=9, a=1, b=2, age=''
Here since classes is object, it has to be looped again to get keys (a,b) and values (1,2).
I tried the following but it gives if any of the values is null, it returns an error:
Cannot convert undefined or null to object
It works well if there is no null value:
function getKeyValues(data) {
var q = '';
f(data);
function f(s) {
Object.keys(s).forEach(function(key) {
if (typeof s[key] === 'object') {
f(s[key]);
} else {
q = q + key + '=' + (s[key] == null) ? "" : s[key] + '&';
}
});
}
return q;
}
d = {
"name": "Sam",
"grade": 9,
"classes": {
"a": 1,
"b": 2
},
"age": null
}
console.log(getKeyValues(d));
Try this one:
function getKeyValues(data) {
var q = [];
var keys = Object.keys(data);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = data[key];
if (value == null) {
q.push(key + "=''");
} else if (typeof value == "object") {
q.push(getKeyValues(value));
} else {
q.push(key + "=" + value);
}
}
return q.join(",");
}
Another approach is to use the reduce method. Personally I find it a little bit cleaner.
function getKeyValues(d) {
return Object.keys(d).reduce((memo, key) => {
if (!d[key]) {
memo[key] = '';
} else if (typeof d[key] === 'object') {
Object.keys(d[key]).forEach((subKey) => {
memo[subKey] = d[key][subKey];
})
} else {
memo[key] = d[key];
}
return memo;
}, {})
}
Also, while your question is very clear, I must say that it also makes me a little bit wary. You could find yourself in some difficult debugging situations if property names are ever repeated in nested objects. For example, if
d={"name":"Sam","grade":9,"buddy":{"name":"Jeff","age":12}}
would you expect name be "Sam" or "Jeff"? A function that answers your question could return either, so that is something to be aware of going forward.

Preserving undefined that JSON.stringify otherwise removes

How do I preserve undefined values when doing JSON.stringify(hash)?
Here's an example:
var hash = {
"name" : "boda",
"email" : undefined,
"country" : "africa"
};
var string = JSON.stringify(hash);
// > '{"name":"boda","country":"africa"}'
Email disappeared from JSON.stringify.
The JSON spec does not allow undefined values, but does allow null values.
You can pass a replacer function to JSON.stringify to automatically convert undefined values to null values, like this:
var string = JSON.stringify(
obj,
function(k, v) { return v === undefined ? null : v; }
);
This works for undefined values inside arrays as well, as JSON.stringify already converts those to null.
You can preserve the key by converting to null since a valid JSON does not allow undefined;
Simple one liner:
JSON.stringify(obj, (k, v) => v === undefined ? null : v)
This should do the trick
// Since 'JSON.stringify' hides 'undefined', the code bellow is necessary in
// order to display the real param that have invoked the error.
JSON.stringify(hash, (k, v) => (v === undefined) ? '__undefined' : v)
.replace(/"__undefined"/g, 'undefined')
Use null instead of undefined.
var hash = {
"name" : "boda",
"email" : null,
"country" : "africa"
};
var string = JSON.stringify(hash);
> "{"name":"boda","email":null,"country":"africa"}"
Im reading between the lines here and guessing that you want to have the value undefined when you use JSON.parse?
If that is the case you could use the following:
var encodeUndefined = function(obj, undefinedPaths, path) {
path = path || 'ROOT';
for (var key in obj) {
var keyPath = path + '.' + key;
var value = obj[key];
if (value === undefined) {
undefinedPaths.push(keyPath);
} else if (typeof value == "object" && value !== null) {
encodeUndefined(obj[key], undefinedPaths, keyPath);
}
}
}
var stringifyAndPreserveUndefined = function(obj) {
var undefinedPaths = [];
//save all paths that have are undefined in a array.
encodeUndefined((obj), undefinedPaths);
return JSON.stringify({
ROOT: obj,
undefinedPaths: undefinedPaths
}, function(k, v) { if (v === undefined) { return null; } return v; });
}
var parseAndRestoreUndefined = function(value) {
var data = JSON.parse(value);
var undefinedPaths = data.undefinedPaths;
var obj = data.ROOT;
//Restore all undefined values
for (var pathIndex = 0; pathIndex < undefinedPaths.length; pathIndex++) {
var pathParts = undefinedPaths[pathIndex].substring(5).split('.');
var item = obj;
for (var pathPartIndex = 0; pathPartIndex < pathParts.length - 1; pathPartIndex++) {
item = item[pathParts[pathPartIndex]];
}
item[pathParts[pathParts.length - 1]] = undefined;
}
return obj;
}
var input = {
test1: 'a',
test2: 'b',
test3: undefined,
test4: {
test1: 'a',
test2: undefined
}
};
var result = stringifyAndPreserveUndefined(input);
var result2 = parseAndRestoreUndefined(result);
stringifyAndPreserveUndefined will encode all undefined values in a array and when you call parseAndRestoreUndefined it will put them in the correct place again.
The one downside is the json will not look exactly like the object. In the example above it will turn into {"ROOT":{"test1":"a","test2":"b","test4":{"test1":"a"}},"undefinedPaths":["ROOT.test3","ROOT.test4.test2"]}
function stringifyWithUndefined(value: any, space: number): string {
const str = JSON.stringify(
value,
(_k, v) => v === undefined ? '__UNDEFINED__' : v,
space
);
return str.replaceAll('"__UNDEFINED__"', 'undefined');
}
Example 1:
const object = {
name: 'boda',
email: undefined,
country: 'africa'
};
console.log(stringifyWithUndefined(object, 2));
Result (string):
{
"name": "boda",
"email": undefined,
"country": "africa"
}
Example 2:
const array = [object, { object }, [[object]]];
console.log(stringifyWithUndefined(array, 2));
Result (string):
[
{
"name": "boda",
"email": undefined,
"country": "africa"
},
{
"object": {
"name": "boda",
"email": undefined,
"country": "africa"
}
},
[
[
{
"name": "boda",
"email": undefined,
"country": "africa"
}
]
]
]
Note that undefined is not valid JSON and JSON.parse() will fail with SyntaxError: Unexpected token [...] is not valid JSON if you give it the result of stringifyWithUndefined()
JSON does not have an undefined value, but we could write a workaround:
Preserving nested undefined values
I wrote 2 functions that internally uses JSON.stringify and JSON.parse and preserves nested undefined values using a value placeholder:
Equivalent to JSON.stringify:
/**
* Serialize a POJO while preserving nested `undefined` values.
*/
function serializePOJO(value, undefinedPlaceholder = "[undefined]") {
const replacer = (key, value) => (value === undefined ? undefinedPlaceholder : value);
return JSON.stringify(value, replacer);
}
Equivalent to JSON.parse:
/**
* Deserialize a POJO while preserving nested `undefined` values.
*/
function deserializePOJO(value, undefinedPlaceholder = "[undefined]") {
const pojo = JSON.parse(value);
if (pojo === undefinedPlaceholder) {
return undefined;
}
// Function that walks through nested values
function deepIterate(value, callback, parent, key) {
if (typeof value === "object" && value !== null) {
Object.entries(value).forEach(([entryKey, entryValue]) => deepIterate(entryValue, callback, value, entryKey));
} else if (Array.isArray(value)) {
value.forEach((itemValue, itemIndex) => deepIterate(itemValue, callback, value, itemIndex));
} else if (parent !== undefined) {
callback(value, parent, key);
}
}
// Replaces `undefined` placeholders
deepIterate(pojo, (value, parent, key) => {
if (value === undefinedPlaceholder) {
parent[key] = undefined;
}
});
return pojo;
}
Usage:
const source = {
foo : undefined,
bar : {
baz : undefined
}
};
const serialized = serializePOJO(source);
console.log("Serialized", serialized);
// '{"foo":"[undefined]","bar":{"baz":"[undefined]","qux":[1,"[undefined]",2]}}'
const deserialized = deserializePOJO(serialized);
console.log("Deserialized", deserialized);
Works with both object entries and array items.
The downside is that you have to choose an appropriate placeholder that will not be mistaken via a "real" source value. The placeholder is customizable via the optional undefinedPlaceholder argument.
This is specially useful to store POJO in browser local storage ;)
See also:
Gist: https://gist.github.com/yvele/f115f7dd0ed849f918f38b134ec3598a
JSFiddle: https://jsfiddle.net/n5jt2sf9/
This will cause it to print as undefined but this is INVALID json, but is valid JavaScript.
var string = JSON.stringify(obj, function(k,v){return v===undefined?"::undefined::":v}, 2).replace(new RegExp("\"::undefined::\"", 'g'), "undefined");

For-in looping through an object stops after first key

Can someone explain how to iterate over all of an object's key-val pairs? Do I need to add the stringified pairs into a new object, then return that object? So far, I've got the following, which works for nested objects, but it'll only return the first pair.
var objectify = function (obj, stringifyJSON) {
for (var key in obj) {
if (obj.hasOwnProperty) {
return (stringifyJSON(key) + ":" + stringifyJSON(obj[key]));
}
As it is, for an object like {"a": 1, "b": 2, "c":3 }, I'm getting back just {"a": 1}.
I've also tried the following:
var objectify = function (obj, stringify) {
var holder = {};
for (var key in obj) {
if (obj.hasOwnProperty) {
holder[stringifyJSON(key)] = stringifyJSON(obj[key]);
}
}
return ("" + holder + "");
};
which just returns {[object Object]}. I've tried assigning the key and value with quotes and without calling stringify on the key and value, but those both return {[object Object]} as well.
Here's the snippet that calls objectify:
else if (typeof obj === "object" && obj !== null) {
return ("{" + objectify(obj, stringifyJSON) + "}");
}
Don't use return in the loop.
A possible solution could be:
var objectify = function (obj, stringifyJSON) {
var tmp = '';
for (var key in obj) {
if (obj.hasOwnProperty) {
tmp = tmp + (stringifyJSON(key) + ":" + stringifyJSON(obj[key]));
tmp =+ '; '; // here you can put any char that you want to separate objects
}
}
return tmp;
}

Categories