mobxjs / serializr

Serialize and deserialize complex object graphs to and from JSON and Javascript classes
MIT License
766 stars 52 forks source link

Serializing "any" type #74

Open evelant opened 6 years ago

evelant commented 6 years ago

Serializr seems to assume that the shape of objects are always fixed and that properties only ever have one type.

What is the best method to serialize a property that may be of any type (ex: could be string, number, null, undefined, array, or obj)?

alexggordon commented 6 years ago

custom is almost definitely the way to go with this.

There's really no way serializr can guess exactly the output you want from a situation like that. Maybe sometimes you want to remove the key from the json with SKIP. Maybe sometimes you want to do something to the object, but I would suggest using custom for this.

evelant commented 6 years ago

Should something like this work?

export function anyType() {
    return {
        serializer: (v,k,obj) => EJSON.stringify(v),
        deserializer: (val, context) => EJSON.parse(val)
    }
}

class MyClass{
    @serializable(anyType()) @observable myAnyType: any
}
alexggordon commented 6 years ago

I don't believe that will work exactly, as it's not json that's usually passed in.

If it's an array it will fail like so:

JSON.parse([])
Uncaught SyntaxError: Unexpected end of JSON input
    at JSON.parse (<anonymous>)
    at <anonymous>:1:6

as that's not valid json. I would suggest not doing json parse/stringify inside a serializr or deserializr. Just return the raw values you want and stringify after serialization/deserialization

evelant commented 6 years ago

Hmm OK maybe I am a little confused about the way serializr works. I was thinking that (de)serialize functions were meant to construct and parse JSON strings. Am I correct now in thinking that the purpose of (de)serialize is to convert values into a structure that can be stringified into valid JSON and to transform the deserialized JSON object back into the original data structure?

As long as I know my property is valid to be serialized to JSON without modification can I use the following?

export function anyType() {
    return {
        serializer: (v,k,obj) => v,
        deserializer: (val, context) => val
    }
}
alexggordon commented 6 years ago

Yes, that is the point of serializr. Serializr actually never calls JSON.parse or JSON.stringify. It's point is entirely to take a plain object (like the result of JSON.parse) and transmute that plain object onto a set of classes, and then back to that object.

In addition to that, while your second solution will work, I would suggest using custom for it.

import { custom  } from 'serializr';

export const anyType = custom(v => v, val => val)
evelant commented 6 years ago

@alexggordon is there a functional difference between the solution I proposed and using custom()?

alexggordon commented 6 years ago

There is no functional difference. However, in the unlikely situation of us changing the format of the object (serializer, deserializer keys) the custom route wouldn't break, whereas manually specifying those keys would.

evelant commented 6 years ago

Hmm, I am still having troubles. I am using serializr with mobx-persist. One of my properties looks like this:

@serializable(anyType()) @observable myProperty: {[key: string]: {[k: string]: string | number}}
@serializable(primitive()) @observable otherProp: string

myProperty is just plain nested js objects that serialize to json without issue. What I am seeing is that myProperty is serialized correctly (verified with browser dev tools in indexedDB), but does not get deserialized correctly.

mobx-persist calls update() like so

        const schema = getDefaultModelSchema(store as any)
        function hydration() {
            const promise: IHydrateResult<T> = storage.getItem(key)
            .then((d: any) => !jsonify ? d : JSON.parse(d))
            .then(action(
                `[mobx-persist ${key}] LOAD_DATA`,
                (persisted: any) => {
                    if (persisted && typeof persisted === 'object') {
                        update(schema, store, persisted, null, customArgs)
                    }
                    mergeObservables(store, initialState)
                    return store
                }
            ))
            promise.rehydrate = hydration
            return promise
        }

The JSON value persisted in this case correctly contains my serialized object. After calling update() my store contains undefined for myProperty. All other (primitive) values deserialize just fine.

alexggordon commented 6 years ago

Sorry to make you go to the trouble, but could you maybe make a codepen/jsfiddle/repl.it of the issue? There's a lot of moving parts that make it pretty hard to help just by looking at the code.

evelant commented 6 years ago

Sure, I'll see if I can make a minimal reproduction