flow-runtime icon indicating copy to clipboard operation
flow-runtime copied to clipboard

Is it possible to check if a JSON object validates against all properties of my class definition?

Open arabold opened this issue 7 years ago • 2 comments

This is a:

  • [ ] Bug Report
  • [ ] Feature Request
  • [X] Question
  • [ ] Other

Which concerns:

  • [X] flow-runtime
  • [X] babel-plugin-flow-runtime
  • [ ] flow-runtime-validators
  • [ ] flow-runtime-mobx
  • [ ] flow-config-parser
  • [ ] The documentation website

What is the current behaviour?

I'm banging my head against the wall for a couple of hours now (yes, it hurts!). I'm trying to implement a validation for JSON objects. I have a class like:

import _ from "lodash";

class MySchema {
  foo: string;
  bar: string;

  assign(payload: Object) {
    _.assign(this, payload);
  }

  doSomething() { ... };
  doSomethingElse() { ... };
}

In the MySchema#assign above I'm now trying to validate a plain object payload = { foo: "foo", bar: "bar" } against that class definition. I've tried a couple of different approaches without much success:

This code only checks that the payload is an instance of MySchema, but doesn't check the properties foo and bar for validity.

const schemaValidator = (reify: Type< MySchema >);
schemaValidator.assert(_.assign(new MySchema(), payload));

There's little documentation but $Shape looked like something useful. Unfortunately the following code just throws an "Can only $Shape<T> object types." exception:

const schemaValidator = (reify: Type< $Shape< MySchema > >);
schemaValidator.assert(payload);

The validation works as expected if I change MySchema to a type instead of a class. But obviously that's not what I want as I'd like to provide additional functions (#doSomething and #doSomethingElse) in the class instance. There seems to be no way in Flow to have a class inherit from a Type.

Another way I found is to define MySchema as an interface instead of a class, but this makes me repeat all properties in the actual class implementation like this:

interface MySchema {
  foo: string;
  bar: string;
}

class MyEntity implements MySchema {
  foo: string; // <-- I have to duplicate all properties here
  bar: string; // <-- yuck

  doSomething() { ... };
  doSomethingElse() { ... };
}

// But at least the validation seems to work now...
const schemaValidator = (reify: Type< MySchema >);
schemaValidator.assert(payload);

I had plenty of other iterations but can't get it to really work intuitively.


What is the expected behaviour?

I would hope to find a simple way of validating that my JSON object is indeed "compatible" with my class definition. Is that possible?


Which package versions are you using?

├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]
├── [email protected]

arabold avatar Oct 01 '17 19:10 arabold

It's noteworthy that I'm disabling annotations in my .babelrc to avoid any runtime performance penalties (not sure if there are any?). It still works perfectly fine if I define MySchema above as a type instead of a class. The validation code properly checks property types:

  "plugins": [
    [ "flow-runtime", { "assert": false, "annotate": false } ]
  ]

I've finally enabled annotations and I seemingly could get classes to work now as well:

  "plugins": [
    "transform-decorators-legacy",
    [ "flow-runtime", { "assert": false, "annotate": true } ]
  ]

After enabling annotate like this the following code actually worked:

const schemaValidator = (reify: Type< MySchema >);
schemaValidator.assert(payload);

But is this intended to work this way? It also doesn't seem to be a good solution as only a handful of classes should be validated against their definition. Most of the code doesn't take external input and I don't want to add any overhead. Guess I'm just confused how to use flow-runtime properly.

arabold avatar Oct 01 '17 23:10 arabold

@arabold Since JSON is based upon plain objects it really only makes sense to assert against plain object types rather than classes. That flow-runtime doesn't support $Shape of a class is a bug, though you'll run into trouble using $Shape of a class to validate JSON because it includes class methods:

/* @flow */
class Foo {
  bar: number
  baz(qux: number): void {}
}
const foo: $Shape<Foo> = {baz: 42}
6: const foo: $Shape<Foo> = {baz: 42}
                            ^ property `baz` of object literal. Covariant property `baz` incompatible with contravariant use in
6: const foo: $Shape<Foo> = {baz: 42}
                     ^ Foo

I'm not sure what you're trying to do with the MySchema class and its methods above and beyond validating against a flow-runtime type, but maybe you could do it this way?

type MySchemaFields = {
  foo: string;
  bar: string;
}

class MySchema {
  fields: MySchemaFields;

  // assertion happens here (unless you have flow-runtime assertions disabled)
  constructor(fields: MySchemaFields) {
    this.fields = fields;
  }

  doSomething() { ... };
  doSomethingElse() { ... };
}

jedwards1211 avatar Jan 22 '18 15:01 jedwards1211