John Little
John Little

Reputation: 12338

How to restore original object/type from JSON?

Its easy to load JSON into an object in javascript using eval or JSON.parse.

But if you have a proper "class" like function, how do you get the JSON data into it?

E.g.

function Person(name) {
  this.name=name;
  this.address = new Array();
  this.friendList;

  this.promote = function(){
     // do some complex stuff
  }
  this.addAddress = function(address) {
    this.address.push(address)
  }
}

var aPersonJSON = '{\"name\":\"Bob\",\"address\":[{\"street\":\"good st\",\"postcode\":\"ADSF\"}]}'

var aPerson = eval( "(" + aPersonJSON + ")" ); // or JSON.parse
//alert (aPerson.name);    // Bob
var someAddress = {street:"bad st",postcode:"HELL"};
//alert (someAddress.street); // bad st
aPerson.addAddress(someAddress); // fail!

The crux is I need to be able to create proper Person instances from JSON, but all I can get is a dumb object. Im wondering if its possible to do something with prototypes?

I dont want to have to parse each line of the JSON and assign each variable to the coresponding functions attributes, which would be too difficult. The actualy JSON and functions I have are much more complicated than the example above.

I am assuming one could JSONify the functions methods into the JSON string, but as I need to keep the resultant data as small as possible this is not an option - I only want to store and load the data, not the javascript code for the methods.

I also dont want to have to put the data loaded by JSON as a sub object if I can help it (but might be the only way), e.g.

function Person(name) {
  this.data = {};
  this.data.name=name;
}

var newPerson = new Person("");
newPerson.data = eval( "(" + aPersonJSON + ")" );
alert (newPerson.data.name); // Bob

Any ideas?

Upvotes: 17

Views: 20128

Answers (8)

user15316261
user15316261

Reputation: 7

The modern approach (in December 2021) is to use @badcafe/jsonizer : https://badcafe.github.io/jsonizer

  • Unlike other solutions, it doesn't pollute you data with injected class names,
  • and it reifies the expected data hierarchy.
  • below are some examples in Typescript, but it works as well in JS

Before showing an example with a class, let's start with a simple data structure :

const person = {
    name: 'Bob',
    birthDate: new Date('1998-10-21'),
    hobbies: [
        {   hobby: 'programming',
            startDate: new Date('2021-01-01'),
        },
        {   hobby: 'cooking',
            startDate: new Date('2020-12-31'),
        },
    ]
}
const personJson = JSON.stringify(person);
// store or send the data

Now, let's use Jsonizer 😍

// in Jsonizer, a reviver is made of field mappers :
const personReviver = Jsonizer.reviver<typeof person>({
    birthDate: Date,
    hobbies: {
        '*': {
            startDate: Date
        }
    }
});
const personFromJson = JSON.parse(personJson, personReviver);

Every dates string in the JSON text have been mapped to Date objects in the parsed result.

Jsonizer can indifferently revive JSON data structures (arrays, objects) or class instances with recursively nested custom classes, third-party classes, built-in classes, or sub JSON structures (arrays, objects).

Now, let's use a class instead :

// in Jsonizer, a class reviver is made of field mappers + an instance builder :
@Reviver<Person>({ // 👈  bind the reviver to the class
    '.': ({name, birthDate, hobbies}) => new Person(name, birthDate, hobbies), // 👈  instance builder
    birthDate: Date,
    hobbies: {
        '*': {
            startDate: Date
        }
    }
})
class Person {
    constructor( // all fields are passed as arguments to the constructor
        public name: string,
        public birthDate: Date
        public hobbies: Hobby[]
    ) {}
}
interface Hobby {
    hobby: string,
    startDate: Date
}

const person = new Person(
    'Bob',
    new Date('1998-10-21'),
    [
        {   hobby: 'programming',
            startDate: new Date('2021-01-01'),
        },
        {   hobby: 'cooking',
            startDate: new Date('2020-12-31'),
        },
    ]
);
const personJson = JSON.stringify(person);

const personReviver = Reviver.get(Person); // 👈  extract the reviver from the class
const personFromJson = JSON.parse(personJson, personReviver);

Finally, let's use 2 classes :

@Reviver<Hobby>({
    '.': ({hobby, startDate}) => new Hobby(hobby, startDate), // 👈  instance builder
    startDate: Date
})
class Hobby {
    constructor (
        public hobby: string,
        public startDate: Date
    ) {}
}

@Reviver<Person>({
    '.': ({name, birthDate, hobbies}) => new Person(name, birthDate, hobbies), // 👈  instance builder
    birthDate: Date,
    hobbies: {
        '*': Hobby  // 👈  we can refer a class decorated with @Reviver
    }
})
class Person {
    constructor(
        public name: string,
        public birthDate: Date,
        public hobbies: Hobby[]
    ) {}
}

const person = new Person(
    'Bob',
    new Date('1998-10-21'),
    [
        new Hobby('programming', new Date('2021-01-01')),
        new Hobby('cooking', new Date('2020-12-31')
    ]
);
const personJson = JSON.stringify(person);

const personReviver = Reviver.get(Person); // 👈  extract the reviver from the class
const personFromJson = JSON.parse(personJson, personReviver);

Upvotes: 0

Nik
Nik

Reputation: 689

TL; DR: This is approach I use:

var myObj = JSON.parse(raw_obj_vals);
myObj = Object.assign(new MyClass(), myObj);

Detailed example:

const data_in = '{ "d1":{"val":3,"val2":34}, "d2":{"val":-1,"val2":42, "new_val":"wut?" } }';
class Src {
    val1 = 1;
    constructor(val) { this.val = val; this.val2 = 2; };
    val_is_good() { return this.val <= this.val2; }
    get pos_val() { return this.val > 0; };
    clear(confirm) { if (!confirm) { return; }; this.val = 0; this.val1 = 0; this.val2 = 0; };
};
const src1 = new Src(2); // standard way of creating new objects
var srcs = JSON.parse(data_in);
// ===================================================================
// restoring class-specific stuff for each instance of given raw data
Object.keys(srcs).forEach((k) => { srcs[k] = Object.assign(new Src(), srcs[k]); });
// ===================================================================

console.log('src1:', src1);
console.log("src1.val_is_good:", src1.val_is_good());
console.log("src1.pos_val:", src1.pos_val);

console.log('srcs:', srcs)
console.log("srcs.d1:", srcs.d1);
console.log("srcs.d1.val_is_good:", srcs.d1.val_is_good());
console.log("srcs.d2.pos_val:", srcs.d2.pos_val);

srcs.d1.clear();
srcs.d2.clear(true);
srcs.d3 = src1;
const data_out = JSON.stringify(srcs, null, '\t'); // only pure data, nothing extra. 
console.log("data_out:", data_out);

  • Simple & Efficient. Compliant (in 2021). No dependencies.
  • Works with incomplete input, leaving defaults instead of missing fields (particularly useful after upgrade).
  • Works with excessive input, keeping unused data (no data loss when saving).
  • Could be easily extended to much more complicated cases with multiple nested classes with class type extraction, etc.
  • doesn't matter how much data must be assigned or how deeply it is nested (as long as you restore from simple objects, see Object.assign() limitations)

Upvotes: 1

GTCrais
GTCrais

Reputation: 2077

A little late to the party, but this might help someone. This is how I've solved it, ES6 syntax:

class Page 
{
   constructor() {
      this.__className = "Page";
   }

   __initialize() {
       // Do whatever initialization you need here.
       // We'll use this as a makeshift constructor.
       // This method is NOT required, though
   }
}

class PageSection
{
   constructor() {
      this.__className = "PageSection";
   }
}

class ObjectRebuilder
{
    // We need this so we can instantiate objects from class name strings
    static classList() {
        return {
            Page: Page,
            PageSection: PageSection
        }
    }

    // Checks if passed variable is object.
    // Returns true for arrays as well, as intended
    static isObject(varOrObj) {
        return varOrObj !== null && typeof varOrObj === 'object';
    }

    static restoreObject(obj) {
        let newObj = obj;

        // At this point we have regular javascript object
        // which we got from JSON.parse. First, check if it
        // has "__className" property which we defined in the
        // constructor of each class
        if (obj.hasOwnProperty("__className")) {
            let list = ObjectRebuilder.classList();

            // Instantiate object of the correct class
            newObj = new (list[obj["__className"]]);

            // Copy all of current object's properties
            // to the newly instantiated object
            newObj = Object.assign(newObj, obj);

            // Run the makeshift constructor, if the
            // new object has one
            if (newObj.__initialize === 'function') {
                newObj.__initialize();
            }
        }

        // Iterate over all of the properties of the new
        // object, and if some of them are objects (or arrays!) 
        // constructed by JSON.parse, run them through ObjectRebuilder
        for (let prop of Object.keys(newObj)) {
            if (ObjectRebuilder.isObject(newObj[prop])) {
                newObj[prop] = ObjectRebuilder.restoreObject(newObj[prop]);
            }
        }

        return newObj;
    }
}

let page = new Page();
let section1 = new PageSection();
let section2 = new PageSection();

page.pageSections = [section1, section2];

let jsonString = JSON.stringify(page);
let restoredPageWithPageSections = ObjectRebuilder.restoreObject(JSON.parse(jsonString));

console.log(restoredPageWithPageSections);

Your page should be restored as an object of class Page, with array containing 2 objects of class PageSection. Recursion works all the way to the last object regardless of depth.

@Sean Kinsey's answer helped me get to my solution.

Upvotes: 6

Roman Riesen
Roman Riesen

Reputation: 76

Just in case someone needs it, here is a pure javascript extend function (this would obviously belong into an object definition).

  this.extend = function (jsonString){
    var obj = JSON.parse(jsonString)
    for (var key in obj) {
        this[key] = obj[key]
        console.log("Set ", key ," to ", obj[key])
        }   
    } 

Please don't forget to remove the console.log :P

Upvotes: 0

Sean Kinsey
Sean Kinsey

Reputation: 38046

You need to use a reviver function:

// Registry of types
var Types = {};

function MyClass(foo, bar) {
  this._foo = foo;
  this._bar = bar;
}
Types.MyClass = MyClass;

MyClass.prototype.getFoo = function() {
  return this._foo;
}

// Method which will provide a JSON.stringifiable object
MyClass.prototype.toJSON = function() {
  return {
    __type: 'MyClass',
    foo: this._foo,
    bar: this._bar
  };
};

// Method that can deserialize JSON into an instance
MyClass.revive = function(data) {
  // TODO: do basic validation
  return new MyClass(data.foo, data.bar);
};

var instance = new MyClass('blah', 'blah');

// JSON obtained by stringifying an instance
var json = JSON.stringify(instance); // "{"__type":"MyClass","foo":"blah","bar":"blah"}";

var obj = JSON.parse(json, function(key, value) {
  return key === '' && value.hasOwnProperty('__type')
    ? Types[value.__type].revive(value)
    : this[key];
});

obj.getFoo(); // blah

No other way really...

Upvotes: 27

Omiga
Omiga

Reputation: 581

I;m not too much into this, but aPerson.addAddress should not work, why not assigning into object directly ?

aPerson.address.push(someAddress);
alert(aPerson.address); // alert [object object]

Upvotes: 0

david
david

Reputation: 18258

Many frameworks provide an 'extend' function that will copy fields over from one object to another. You can combine this with JSON.parse to do what you want.

newPerson = new Person();
_.extend(newPerson, JSON.parse(aPersonJSON));

If you don't want to include something like underscore you can always copy over just the extend function or write your own.

Coffeescript example because I was bored:

JSONExtend = (obj, json) ->
  obj[field] = value for own field, value of JSON.parse json
  return obj

class Person
  toString: -> "Hi I'm #{@name} and I'm #{@age} years old."


dude = JSONExtend new Person, '{"name":"bob", "age":27}'
console.log dude.toString()

Upvotes: 6

Qpirate
Qpirate

Reputation: 2078

Easiest way is to use JSON.parse to parse your string then pass the object to the function. JSON.parse is part of the json2 library online.

Upvotes: 2

Related Questions