-1

Let's say I have a nested JavaScript object, like this:

{
  "?xml": {
    "@version": "1.0",
    "@encoding": "UTF-8"
  },
  "Customer": {
    "@xmlns": "http://NamespaceTest.com/CustomerTypes",
    "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
    "Name": {
      "#text": "Name1"
    },
    "DeliveryAddress": {
      "Line1": {
        "@xmlns": "http://NamespaceTest.com/CommonTypes",
        "#text": "Line11"
      },
      "Line2": {
        "@xmlns": "http://NamespaceTest.com/CommonTypes",
        "#text": "Line21"
      }
    }
  }
}

I would like to define a list of properties by name, e.g. ["?xml", "@xmlns"] and have those properties deleted from the structure, so that I get the following output:

{
  "Customer": {
    "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
    "Name": {
      "#text": "Name1"
    },
    "DeliveryAddress": {
      "Line1": {
        "#text": "Line11"
      },
      "Line2": {
        "#text": "Line21"
      }
    }
  }
}

I know I can do it using JSON.stringify(), like this:

function replacer(key, value) {
  if (key === "?xml" || key === "@xmlns") {
    return undefined;
  }
  return value;
}

var filtered = JSON.parse( JSON.stringify( original, replacer ) );

But I don't like that the result is first converted to a string and then has to be parsed back into an object. Is there a function that can filter a data structure like JSON.stringify() does, but which returns an object instead of a string?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
StellaMaris
  • 877
  • 2
  • 12
  • 29
  • 1
    This question is asked repeatedly. Basically create a new object from the old, stringfy it. If you try to do it yourself and become stuck then ask. As it stands this isn't really a question, it's a do this for me – Liam Aug 23 '16 at 13:54
  • 4
    FYI something isn't JSON unless it's a string. Those are just Javascript objects – Liam Aug 23 '16 at 13:56
  • Its not original of `XML` and go learn http://schoolofdata.org/2013/11/21/xml-and-json/ ... can you use xml variable normal is equal json, use `XMLHttpRequest`. – KingRider Aug 23 '16 at 15:38
  • @Liam your objection isn't relevant for this post, in this context there is no initialization of a java script object, so what you see is a [JSON](https://en.wikipedia.org/wiki/JSON) object. I will not discuss this in detail further. – StellaMaris Aug 23 '16 at 23:23
  • 2
    There is no such thing as a JSON "object". JSON is a string notation, so it's either a JSON **string** or a JavaScript **object**. That Wikiepdia aritcle (BTW hardly the font of all truth especially in relation to software) states *human-readable **text*** – Liam Aug 24 '16 at 07:44
  • 1
    @Liam But there is JSON object, [the one and only](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) ; ). – Teemu Aug 24 '16 at 09:31
  • 1
    Hi, StellaMaris. Based on your comments and your self-answer below, I've edited your question to try to clarify its intent, and hopefully prevent it from being closed. If I've made any mistakes, or if there's anything about my edits that you don't agree with, feel free to fix or revert them. Thanks! – Ilmari Karonen Aug 24 '16 at 23:14
  • @IlmariKaronen thx i'm fine with that. – StellaMaris Aug 29 '16 at 08:15

2 Answers2

1

As far as I know, there's no built-in method in JavaScript to deep filter a nested data structure like JSON.stringify() does when given a replacer callback. That said, it's not too hard to write your own:

function filterClone (data, replacer) {
    // return primitives unchanged
    if ( !(data instanceof Object) ) return data;

    // don't try to clone anything except plain objects and arrays
    var proto = Object.getPrototypeOf(data);
    if (proto !== Object.prototype && proto !== Array.prototype) return data;

    // it's a "plain object" or an array; clone and filter it!
    var clone = (proto === Object.prototype ? {} : []);
    for (var prop in data) {
        // safety: ignore inherited properties, even if they're enumerable
        if (!data.hasOwnProperty(prop)) continue;

        // call the replacer to let it modify or exclude the property
        var value = replacer(prop, data[prop]);

        if (value === undefined) continue;
        if (value instanceof Object) value = filterClone(value, replacer);
        clone[prop] = value;
    }
    return clone;
}

The recursive function above will deep-clone any "JSON-like" data structure (i.e. one that consists of only plain {} objects, [] arrays and primitive types like numbers, strings and booleans), filtering it using exactly the same kind of replacer callback as JSON.stringify(). That is, given a JSON-like object original as in your question, you can create a filtered copy of it like this:

function replacer (key, value) {
    if (key === "?xml" || key === "@xmlns") return undefined;
    else return value;
}

var filtered = filterClone(original, replacer);

Note that the "deep clone" created by this function is not perfect (because it's hard to clone arbitrary objects in JavaScript), and there are a few corner cases to beware of:

  • This function only clones objects directly inheriting from Object or Array (including "plain objects" and arrays created using {} and []). Anything else, including primitive values and any objects of any other type, are simply copied to the output structure without being cloned.

    For primitive values, this is harmless, since those are immutable anyway; but if your data structure happens to include, say, Date objects (which are mutable), then those will not be automatically cloned. Thus, modifying the dates in the cloned data structure (using e.g. setTime()) can affect the dates in the original, and vice versa:

    var original = { "date" : new Date("1970-01-01T00:00:00.000Z") };
    var clone = filterClone( original, function (key, val) { return val } );
    console.log( original === clone );            // -> false
    console.log( original.date === clone.date );  // -> true (!)
    console.log( original.date.getTime() );       // -> 0
    clone.date.setYear(2016);
    console.log( original.date.getTime() );       // -> 1451606400000
    

    Of course, you can work around this in your replacer callback, e.g. like this:

    function replacer (key, value) {
        // this is how you clone a date in JS:
        if (value instanceof Date) value = new Date(value.getTime());
        return value;
    }
    
  • Also, the filterClone() function above will not clone any non-enumerable properties in your objects, and any (enumerable) properties with a non-standard descriptor will be replaced by standard properties (with no getters, setters, write restrictions, etc.) in the clone. Now, normal plain objects created using the {} object literal syntax should not have any such fancy property descriptors, but if you added any afterwards, be aware that they will not be cloned. (Obviously enough, symbols will also not be cloned.)

  • If your original object contains a reference to the same plain object or array twice, they will become separate objects / arrays in the clone. For example:

    var sharedObject = {};
    var original = { "foo" : sharedObject, "bar" : sharedObject };
    var clone = filterClone( original, function (key, val) { return val } );
    console.log( original.foo === original.bar ); // -> true
    console.log( clone.foo === clone.bar );       // -> false
    
  • Also, if your objects are not well founded (e.g. if they contain a reference to themselves), it's possible for filterClone() to get stuck in an infinite recursion forever (or until it hits the recursion limit, anyway). For example, here's one simple way to create an object that cannot be cloned using filterClone():

    var foo = {};
    foo.foo = foo;
    
  • Finally, due to the JSON.stringify() replacer callback interface (which the code above follows faithfully) using undefined as a special value meaning "omit this property", it's not possible to properly clone an object containing undefined values using filterClone(). Cloning false or null values works fine, though:

    var original = { "foo" : undefined, "bar" : null };
    var clone = filterClone( original, function (key, val) { return val } );
    console.log( clone ); // -> Object { bar: null }
    

    (While testing this, however, I did discover a bug in my original implementation: apparently, Object.getPrototypeOf(null) throws a TypeError. Moving the instanceof check before the prototype check fixed this issue.)

Still, except for the last one, most of these issues are shared by most other JS deep clone implementations, including JSON.parse( JSON.stringify( obj ) ). As noted above, deep cloning arbitrary objects is hard, particularly in a language like JavaScript that has no standard way to mark an object as cloneable, and which is so flexible in allowing objects to contain all sorts of weird properties. Still, for "simple" objects (notably including anything returned by parsing a valid JSON string), this function should do the trick.


Ps. Of course, one way to side-step most of these problems is to do the filtering in place:

function filterInplace (data, replacer) {
    // don't try to filter anything except plain objects and arrays
    if ( !(data instanceof Object) ) return;
    var proto = Object.getPrototypeOf(data);
    if (proto !== Object.prototype && proto !== Array.prototype) return;

    // it's a "plain object" or an array; filter it!
    for (var prop in data) {
        // safety: ignore inherited properties, even if they're enumerable
        if (!data.hasOwnProperty(prop)) continue;

        // call the replacer to let it modify or exclude the property
        data[prop] = replacer(prop, data[prop]);

        if (data[prop] instanceof Object) filterInplace(data[prop], replacer);
        if (data[prop] === undefined) delete data[prop];
    }
}

This function does not return anything; rather, it just modifies the data structure passed in as the first parameter. It does have a few quirks of its own:

  • It still doesn't even try to filter anything but "plain" objects and arrays.
  • It still breaks on non-well-founded structures (like any recursive solution that doesn't explicitly keep track of which objects it has visited already).
  • For the same reason, if the same object is referenced twice from the data structure, it will be filtered twice. (However, since no cloning is involved, the two references will keep pointing to the same object.)
  • It still doesn't filter non-enumerable properties (or symbols); they will just remain untouched as they are.
  • It may also fail to properly filter other properties with a non-default descriptor (e.g. if they're non-writable, or have getters or setters that do something funny).

There's also a minor difference between the filterClone() and filterInplace() functions above regarding the removal of elements from the end of an array: filterClone() will shorten the array, while filterInplace() will always leave a null value where the removed element was. It's a bit debatable what the "correct" behavior in this case should be; FWIW, JSON.stringify() doesn't shorten the array, either.

Community
  • 1
  • 1
Ilmari Karonen
  • 49,047
  • 9
  • 93
  • 153
0

Here is a solution.

var json = '{"?xml": {"@version": "1.0","@encoding": "UTF-8"},"Customer": {"@xmlns": "http://NamespaceTest.com/CustomerTypes","@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance","Name": {"#text": "Name1"},"DeliveryAddress": {"Line1": {"@xmlns": "http://NamespaceTest.com/CommonTypes","#text": "Line11"},"Line2": {"@xmlns": "http://NamespaceTest.com/CommonTypes","#text": "Line21"}}}}';

var obj = JSON.parse(json);

function RemoveNameSpace(_obj) {
  var _this = _obj;
  for (var p in _this) {
    if (p == "?xml" || p == "@xmlns") {
      delete _this[p];
    }
    if (typeof(_this[p]) == 'object') {
      RemoveNameSpace(_this[p])
    }
  }
  return _this;
}

var newjson = JSON.stringify(RemoveNameSpace(obj));

console.log(newjson);
Pugazh
  • 9,453
  • 5
  • 33
  • 54