convert javascript plain object into model class instance
Asked Answered
S

8

43

I need to implement small ODM like feature. I get plain javascript object from database, and I need to convert it into my model class instance. Let's assume model looks like:

    class Model{
       constructor(){
           this.a = '777';
           ---- whole bunch of other things ---
       }
       print(){
           console.log(this.a);
       }
   }

So I need convert var a = {b:999, c:666} to instance of model and being able to call a.print() after, and when a.print() executed 777 should be placed in console. How to do that?

Speechmaker answered 18/11, 2015 at 11:31 Comment(6)
How could {b:999, c:666} become a Model instance? Your Models only have an a property, not b or c ones. Maybe that's why people don't understand your question.Dunagan
@Dunagan it could be dozens of fields in objects and all of them should not be listed in constructor i think.Speechmaker
@silent_coder: Of course all your fields should be listed in the constructor? An instance wouldn't have those fields if they weren't created.Dunagan
@Dunagan It's javascript mate. You could type this.b = xxx in any method and it will be perfectly valid.Speechmaker
This looks like a duplicate of Casting plain objects to function instances (“classes”) in javascript to me (nothing is different in ES6). Please tell me whether that helps.Dunagan
I'm surprised this question has gotten this little attention in about 3 years.Ardennes
S
50

There have a simple method. Just assign the object to instance(this)

class Model
{
  constructor(obj){
    Object.assign(this, obj)
  }
  print(){
    console.log(this.a);
  }
}

let obj = {a: 'a', b: 'b', c: 'c'}
    
let m = new Model(obj)
console.log(m)
m.print()  // 'a'
Scotism answered 15/8, 2017 at 7:54 Comment(0)
S
8

If I understand the question correctly, you can export a factory function and make use of Object.assign to extend your base Model:

// Export the factory function for creating Model instances
export default const createModel = function createModel(a) {
  const model = new Model();
  return Object.assign(model, a);
};
// Define your base class
class Model {
  constructor() {
    this.a = 777;
  }
  print() {
    console.log(this.a, this.b, this.c)
  }
}

And call it like:

const myModel = createModel({ b: 999, c: 666 });
myModel.print();

Babel REPL Example

Or, of course, you could forego the factory and pass a in as a parameter (or rest parameters) to the constructor but it depends on your preferred coding style.

Shout answered 18/11, 2015 at 12:22 Comment(4)
I would recommend class Model { static create(a) { … } … }, and maybe a more descriptive name like createFrom or fromJSONDunagan
What will be in the case if a.a intially contains different value. Let's say '-500' ? I think that some data will be losted. Or which was in plain object, or which was set in constructor. And how does this case will work if model with time will be extended with properties and read only getters with the same name as fields in plain object?Papoose
@Papoose None of that is specified in the question. Either way, it would be trivial to transform the source object in Object.assign to exclude keys.Shout
@Ph0en1x: Just try it out? The last value will overwrite the previous. And extending instances later (after construction) is an antipattern. Read-only getters will of course cause issues, you have to adapt your createModel method to work with them explicitly.Dunagan
S
4

If you need to typecast more consistently, you can also create your own typecast function like generic function

function typecast(Class, obj) {
  let t = new Class()
  return Object.assign(t,obj)
}

// arbitrary class
class Person {
 constructor(name,age) {
   this.name = name
   this.age = age
 }
 print() {
   console.log(this.name,this.age)
 }
}

call it to typecast any object to any class instance like

let person = typecast(Person,{name:'Something',age:20})
person.print() // Something 20
Skimpy answered 28/6, 2020 at 14:8 Comment(0)
E
3

I would suggest rewriting your class to store all its properties in a single JS object this.props and accept this object in its constructor:

class Model {
  constructor (props = this.initProps()) {
    this.props = props
    // other stuff
  }
  initProps () {
    return {a: '777'}
  }
  print () {
    console.log(this.props.a)
  }
}

Then you'll be able to store this.props in your database as a plain JS object and then use it to easily recreate corresponding class instance:

new Model(propsFromDatabase)

Though, if you don't want to move all properties to this.props, you could use Object.assign to keep your object plain:

class Model {
  constructor (props = this.initProps()) {
    Object.assign(this, props)
    // other stuff
  }
  initProps () {
    return {a: '777'}
  }
  print () {
    console.log(this.a)
  }
}

But I would recommend using the former approach, because it'll keep you safe from name collisions.

Exhibitionist answered 18/11, 2015 at 12:54 Comment(10)
This doesn't default each property separately, but the whole props object only.Dunagan
@Dunagan yes, it's what you would expect from ODM - either read everything from DB, or create from a scratch.Exhibitionist
Also, new Model({b:999, c:666}).print() // a === undefinedShout
@RGraham yes, because it's in this.props.a. But good point, anyway. Added alternative approach.Exhibitionist
This approach have serious disadvantage. If you need to use your property later, like (new Model(a)).b you will need to implement custom getter for any property. That's a big amount of work.Papoose
@LeonidBeschastny No, I mean if you don't specify a in the object passed to props it never gets assigned - babeljs.io/repl/…Shout
@RGraham I see now. Well, in most cases I would expect such behavior. Why it should be present in loaded object if it wasn't present in the saved one?Exhibitionist
@LeonidBeschastny True. Defaults maybe. Who knows. Clearly a poor question if there are so many different answers & unknowns.Shout
@Papoose I agree with you that it'll require some additional work, but there is no need to implement separate getter and setter for every property, because you could either implement one abstract getter function (i.e. this.get('a')), or implement a simple getters factory. In any complex project I would rather stick to this approach and use gettesr\setters, than allow name collisions to occur.Exhibitionist
@LeonidBeschastny Using unified getter not good because it's not refactorable and don't friend well with minifying. Sometimes migrating from plain objects to models is a part of refactoring. In that case it will be needed to rewrite a huge amount of code (was doing similar a little time ago). So using this pattern for my point of view is like reinventing your own ODM, model goes more complex, restrcitive, etc. Too much for javascript.Papoose
T
2

Edit 2024: I think my original idea is still good for this specific use case but in general the following (written in TypeScript) might be better:

class Model {
  a = 11;
  b = 22;
  c = 33;
  constructor(values: Record<"a" | "b" | "c", number> = {}) {
    for (key in values) {
      // to omit extra keys
      if (!(key in this)) continue;
      // to prevent injecting code
      if (typeof this[key] === "function") continue;
      this[key] = values[key];
    }
  }
  print() {
    console.log(this.a)
  }
}

Unfortunately I haven’t tested this so there might be a type error in the for..in loop, but I don’t think there is. In any case it’s not like my original solution would work in TypeScript :)

Original answer:


You could have a static Model.from or Model.parse method, that returns a new Model with those properties:

class Model {
  static defaults = { a: 777, b: 888, c: 999, d: 111, e: 222 };
  constructor() {
    const { defaults } = Model;
    for (const key in defaults) this[key] = defaults[key];
  }
  print() {
    console.log(this.a);
  }
  static from(data) {
    return Object.assign(
      new Model(),
      Model.defaults,
      Object.fromEntries(
        Object.entries(data).filter(([key]) => key in Model.defaults)
      )
    );
  }
}

const data = {
  a: "a", b: "b", c: "c", ajkls: "this wont be included"
};
const myModel = Model.from(data);
console.log("myModel =", myModel);
console.log("myModel instanceof Model:", myModel instanceof Model);
console.log("myModel.print():")
myModel.print();
Tshombe answered 16/9, 2021 at 18:55 Comment(0)
R
1

How about this?:

var a = Object.create(Model.prototype, {
    b: {
        enumerable: true, // makes it visible for Object.keys()
        writable: true, // makes the property writable
        value: 999
    }, c: {
        value: 666
    }
});

You'd be basically creating a new instance of Model from it's prototype and assigning your new properties to it. You should be able to call print as well.

Rodmur answered 18/11, 2015 at 11:51 Comment(2)
I need to write automated code to convert pojso into model instances. I couldn't fill properties manually.Speechmaker
Yes, the properties can also be modified with enumerable, writable, configurable and get and setlike this: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…Rodmur
H
0

Just like G_hi3's answer, but it "automates" the creation of the properties object

function Model() {
  this.a = '777';
}

Model.prototype.print = function(){
    console.log(this.a);
}

   // Customize this if you don't want the default settings on the properties object.
function makePropertiesObj(obj) {
    return Object.keys(obj).reduce(function(propertiesObj, currentKey){
        propertiesObj[currentKey] = {value: obj[currentKey]};
        return propertiesObj;
    }, {}); // The object passed in is the propertiesObj in the callback
}

var data = {a: '888'};

var modelInstance = Object.create(Model.prototype, makePropertiesObj(data));
// If you have some non trivial initialization, you would need to call the constructor. 
Model.call(modelInstance);
modelInstance.print(); // 888
Hemorrhoidectomy answered 18/11, 2015 at 12:25 Comment(3)
This still lacks a call to the constructor, though.Dunagan
@Dunagan I intentionally didn't call the constructor because it would override the data passed in this.a = '777';.Hemorrhoidectomy
You could (should) call it before you pass in the data, I meant. If you don't call it, you might lack initialisation of your instance.Dunagan
G
0

First declare a class in which you want to convert JSON:

class LoginResponse {
  constructor(obj) {
    Object.assign(this, obj);
  }
  access_token;
  token_type;
  expires_in;
}

Now convert the general javascript object into your desired class object:

const obj = {
  access_token: 'This is access token1',
  token_type: 'Bearer1',
  expires_in: 123,
};
  let desiredObject = new LoginResponse(obj);
  console.log(desiredObject);

Output will be:

 LOG  {"access_token": "This is access token1", "expires_in": 123, "token_type": "Bearer1"}
Gamboge answered 27/6, 2022 at 7:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.