Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deserialising/validating/decoding JSON strings to unionize types #41

Open
OliverJAsh opened this issue May 16, 2018 · 6 comments
Open

Comments

@OliverJAsh
Copy link
Collaborator

OliverJAsh commented May 16, 2018

Do you have any suggestions how to validate values as unionize types? Here is a real world example of where this is needed.

I have a union type to represent a page modal.

const Modal = unionize({
    Foo: ofType<{ foo: number }>(),
    Bar: ofType<{ bar: number }>()
});
type Modal = typeof Modal._Union;

The modal is specified to the application through the URL as a query param, e.g. http://foo.com/?modal=VALUE, where VALUE is an object after it has been JSON stringified and URI encoded, e.g.

`?modal=${encodeURIComponent(JSON.stringify(Modal.Foo({ foo: 1 })))}`
// ?modal=%7B%22tag%22%3A%22Foo%22%2C%22foo%22%3A1%7D

In my application I want to be able to match against the modal using the match helpers provide by unionize. However, it is not safe to do so, because the modal query param could be a string containing anything, e.g. modal=foo.

For this reason I need to validate the value before matching against it.

In the past I've enjoyed using io-ts for the purpose of validating. I am aware you also have a similar library called runtypes.

If this is a common use case, I wonder if there's anything we could work into the library, or build on top of it, to make it easier.

Here is a working example that uses io-ts and tries to share as much as possible, but duplication is inevitable and it's a lot of boilerplate.

import { ofType, unionize } from "unionize";
import * as t from "io-ts";
import { option } from "fp-ts";

//
// Define our runtime types, for validation
//

const FooRT = t.type({ tag: t.literal("Foo"), foo: t.number });
type Foo = t.TypeOf<typeof FooRT>;

const BarRT = t.type({ tag: t.literal("Bar"), bar: t.number });
type Bar = t.TypeOf<typeof BarRT>;

const ModalRT = t.taggedUnion("tag", [FooRT, BarRT]);

//
// Define our unionize types, for object construction and matching
//

export const Modal = unionize({
    Foo: ofType<Foo>(),
    Bar: ofType<Bar>()
});
export type Modal = typeof Modal._Union;

//
// Example of using the unionize object constructors
//

const modalFoo = Modal.Foo({ foo: 1 });

//
// Example of validation with io-ts + matching with unionize
//

const parseJsonSafe = (str: string) => option.tryCatch(() => JSON.parse(str));

const validateModal = (str: string) => {
    console.log("validating string:", str);

    parseJsonSafe(str).foldL(
        () => {
            console.log("invalid json");
        },
        parsedJson => {
            ModalRT.decode(parsedJson).fold(
                () => console.log("parsed json, invalid modal"),
                modal =>
                    Modal.match({
                        Foo: foo =>
                            console.log("parsed json, valid modal foo", foo),
                        Bar: bar =>
                            console.log("parsed json, valid modal bar", bar)
                    })(modal)
            );
        }
    );
};

validateModal(JSON.stringify({ tag: "Foo", foo: 1 }));
/*
validating string: {"tag":"Foo","foo":1}
parsed json, valid modal foo { tag: 'Foo', foo: 1 }
*/
validateModal(JSON.stringify({ tag: "Bar", bar: 1 }));
/*
validating string: {"tag":"Bar","bar":1}
parsed json, valid modal bar { tag: 'Bar', bar: 1 }
*/
validateModal("INVALID JSON TEST");
/*
validating string: INVALID JSON TEST
invalid json
*/
@pelotom
Copy link
Owner

pelotom commented May 16, 2018

The rule I try to live by with my libraries is that they should do one very narrowly focused thing and (hopefully) do it well. This is mostly out of necessity, because when scope explodes so does the maintenance burden, but I also prefer to use libraries like that, because they compose well together and can be readily swapped out for new solutions as needed. So anyway, I think this is a great case for a composite library which leverages either io-ts or runtypes. Since it looks like io-ts has a tagged union primitive whereas runtypes doesn't, maybe that's a better starting point. An io-unionize library could theoretically take any t.taggedUnion and produce from it a unionized instance. What do you think?

@OliverJAsh
Copy link
Collaborator Author

Agreed!

take any t.taggedUnion and produce from it a unionized instance

How do you think this would look?

@sledorze
Copy link
Contributor

@pelotom @OliverJAsh that would be great!

@pelotom
Copy link
Owner

pelotom commented May 17, 2018

How do you think this would look?

I'm not sure what it would look like for io-ts, but in runtypes all types are backed by a Reflect instance with fields that allow writing runtime algorithms against them. I would imagine a tagged union runtime type would have a tagName: string field and a values: Record<string, Runtype> field which maps tag values to variant types... these are the essential ingredients needed to pass to unionize.

I don't have the bandwidth to investigate this more fully, just spitballin' here 😄

@OliverJAsh
Copy link
Collaborator Author

I filed an issue with io-ts to see if there's a way we can incorporate the benefits of unionize into io-ts, so we have the best of both worlds:

gcanti/io-ts#187

@OliverJAsh
Copy link
Collaborator Author

For anyone who is interested, this might be helpful: gcanti/io-ts#187

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants