Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Automatic JSON de/serialization with Purescript

Justin Woo
February 03, 2017

Automatic JSON de/serialization with Purescript

Small talk about deriving Generics.Rep instances and doing automatic JSON de/serialization with Purescript

https://github.com/justinwoo/purescript-howto-foreign-generic

Justin Woo

February 03, 2017
Tweet

More Decks by Justin Woo

Other Decks in Programming

Transcript

  1. Automatic JSON
    de/serialization
    with Purescript
    Justin Woo
    3 Feb 17

    View full-size slide

  2. What do you mean by “de/serialization”?
    We should be able to take a given type and…
    ● Convert values of the type to JSON (serialize)
    ● Convert JSON to a result of values of the type (deserialize)
    What I don’t want
    ● JSON.parse as String -> a
    ○ Just pretending my arbitrary JS object is the correct type/shape is not fun
    ○ undefined is not a function
    ○ cannot read property ‘sanity’ of undefined
    ○ etc.

    View full-size slide

  3. But we’ve been doing that for ages!
    If you’re not a JS programmer, then yes, you probably have been
    ● Manually decoding JSON
    ○ String -> Hashmap/Object/“Foreign” -> validate/extract properties -> construct object or throw
    IllegalArgumentException or Result or what have you
    ■ You really should not be doing this as it’s manual and error-prone
    ● Annotation-based
    ○ Java devs should be doing this with Jackson annotations and such
    ○ “I do this with Joi/tcomb schemas”
    ● “Generic”/“Shapeless”/type-level introspection-based
    ○ Scala: JSON to case class or w/e, Haskell: Aeson, etc.
    ○ Purescript with Foreign-Generic
    ○ Rust json::decode?

    View full-size slide

  4. Brief primer
    Purescript-foreign uses IsForeign and AsForeign typeclass instances for
    de/serialization. Typeclasses in a nutshell: “it’s kind of like Interfaces” (cue booing)
    Currently two Generics approaches exist (might/probably will change by the time
    someone revisits these slides)
    1. Generic (existing)
    2. Generic.Rep (newer)
    a. enables a lot more powerful stuff
    b. like deriving Is/AsForeign instances for my cruddy JSON

    View full-size slide

  5. A simple record example
    Imagine we have a simple record (modeling JSON from a config file, HTTP request,
    etc.):
    newtype SimpleRecord = SimpleRecord
    { a :: Int
    , b :: String
    , c :: Boolean
    }

    View full-size slide

  6. Derive Generic.Rep
    Deriving a Generic.Rep instance is as easy as
    import Data.Generic.Rep as Rep
    derive instance repGenericSimpleRecord :: Rep.Generic SimpleRecord _
    Now we can use any Rep.Generic a _ => [...]
    readGeneric :: forall a rep. (Generic a rep, GenericDecode rep) => Options -> Foreign -> F a
    toForeignGeneric :: forall a rep. (Generic a rep, GenericEncode rep) => Options -> a -> Foreign

    View full-size slide

  7. IsForeign/AsForeign specifics
    class IsForeign a where
    read :: Foreign -> F a
    Type with instance for IsForeign must have an implementation for read
    class AsForeign a where
    write :: a -> Foreign
    Same, but for write

    View full-size slide

  8. Writing instances accordingly
    instance isForeignSimpleRecord :: IsForeign SimpleRecord where
    read = readGeneric $ defaultOptions {unwrapSingleConstructors = true}
    instance asForeignSimpleRecord :: AsForeign SimpleRecord where
    write = toForeignGeneric $ defaultOptions {unwrapSingleConstructors = true}
    Options actually quite useful in some cases, as we’ll see later
    Only four lines of code to do this very explicitly!

    View full-size slide

  9. Reading JSON
    As simple as
    readJSON :: forall a. IsForeign a => String -> F a
    type F a = Except MultipleErrors a
    type MultipleErrors = NonEmptyList ForeignError
    Extracting the result: just run it!
    runExcept :: forall e a. Except e a -> Either e a

    View full-size slide

  10. Putting our instance to work
    testJSON original input expected = do
    log' "can be converted to JSON" (show original) json
    it "can be converted back" $ readJSON' json `shouldEqual` Right
    original
    it' "can be converted from JSON" input expected $ readJSON'
    input `shouldEqual` expected
    where
    readJSON' = runExcept <<< readJSON
    json = unsafeStringify <<< write $ original
    describe "SimpleRecord" do
    testJSON
    (SimpleRecord { a: 1, b: "b", c: true })
    "{ \"a\": 123, \"b\": \"abc\", \"c\": false }"
    (Right (SimpleRecord { a: 123, b: “abc", c: false }))
    SimpleRecord
    ✓ can be converted to JSON
    (SimpleRecord { a: 1, b: "b", c: true })
    -> {"c":true,"b":"b","a":1}
    ✓ can be converted back
    ✓ can be converted from JSON
    { "a": 123, "b": "abc", "c": false }
    -> (Right (SimpleRecord { a: 123, b: "abc", c: false }))

    View full-size slide

  11. But wait, there’s more!
    Things that should reasonably work easily:
    ● Nested Records
    newtype NestedRecord = NestedRecord
    { d :: SimpleRecord }
    ● Arrays and Null/Undefined values
    newtype RecordWithArrayAndNullOrUndefined = RecordWithArrayAndNullOrUndefined
    { intArray :: Array Int
    , optionalInt :: NullOrUndefined Int }

    View full-size slide

  12. More Results
    NestedRecord
    ✓ can be converted to JSON
    (NestedRecord { d: (SimpleRecord { a: 1, b: "b", c: true }) })
    -> {"d":{"c":true,"b":"b","a":1}}
    ✓ can be converted back
    ✓ can be converted from JSON
    { "d": { "a": 123, "b": "abc", "c": false } }
    -> (Right (NestedRecord { d: (SimpleRecord { a: 123, b: "abc",
    c: false }) }))
    RecordWithArrayAndNullOrUndefined
    ✓ can be converted to JSON
    (RecordWithArrayAndNullOrUndefined { intArray: [1,2,3],
    optionalInt: (NullOrUndefined (Just 1)) })
    -> {"optionalInt":1,"intArray":[1,2,3]}
    ✓ can be converted back
    ✓ can be converted from JSON
    { "intArray": [1, 2, 3] }
    -> (Right (RecordWithArrayAndNullOrUndefined {
    intArray: [1,2,3], optionalInt: (NullOrUndefined Nothing) }))

    View full-size slide

  13. What about “union types”/ADTs?
    There are two primary ways ADTs are represented in people’s code today
    ● Constants
    ○ “Apple”, “Banana”, “Grape”
    ○ Takes some effort, though there are some ways it could be done that are less time-efficient
    ● Tagged Objects
    ○ { “tag”: “Fruit”, contents: { “color”: “red” } }
    ○ We can do this automatically

    View full-size slide

  14. Case of string constants
    data Fruit
    = Apple
    | Banana
    | Watermelon
    derive instance repGenericFruit :: Rep.Generic Fruit _
    instance showFruit :: Show Fruit where
    show = genericShow
    instance isForeignFruit :: IsForeign Fruit where
    read x = chooseFruit =<< readString x
    where
    chooseFruit s
    | s == show Apple = pure Apple
    | s == show Banana = pure Banana
    | s == show Watermelon = pure Watermelon
    | otherwise = fail $
    ForeignError "We don't know what fruit this is!!!"
    instance asForeignFruit :: AsForeign Fruit where
    write = toForeign <<< show
    newtype RecordWithADT = RecordWithADT
    { fruit :: Fruit }
    [...]

    View full-size slide

  15. And then use it!
    describe "RecordWithADT" do
    testJSON
    (RecordWithADT { fruit: Apple })
    "{ \"fruit\": \"Watermelon\" }"
    (Right (RecordWithADT { fruit: Watermelon }))
    RecordWithADT
    ✓ can be converted to JSON
    (RecordWithADT { fruit: Apple })
    -> {"fruit":"Apple"}
    ✓ can be converted back
    ✓ can be converted from JSON
    { "fruit": "Watermelon" }
    -> (Right (RecordWithADT { fruit: Watermelon }))

    View full-size slide

  16. Case of tagged objects
    Exactly the same as normal records,except one thing
    data ADTWithArgs
    = Increment
    | Add Int
    | Set { count :: Int }
    | Reset
    derive instance genericRepADTWithArgs :: Rep.Generic ADTWithArgs _
    instance isForeignADTWithArgs :: IsForeign ADTWithArgs where
    read = readGeneric defaultOptions
    instance asForeignADTWithArgs :: AsForeign ADTWithArgs where
    write = toForeignGeneric defaultOptions
    Using the default option for unwrapSingleConstructors = false

    View full-size slide

  17. Result
    describe "ADTWithArgs" do
    testJSON
    (Set { count: 5 })
    "{ \"tag\": \"Add\", \"contents\": 123 }"
    (Right (Add 123))
    ADTWithArgs
    ✓ can be converted to JSON
    (Set { count: 5 })
    -> {"contents":{"count":5},"tag":"Set"}
    ✓ can be converted back
    ✓ can be converted from JSON
    { "tag": "Add", "contents": 123 }
    -> (Right (Add 123))

    View full-size slide

  18. We can do that
    data TypicalJSTaggedObject
    = Logout
    | Login
    { username :: String
    , password :: String
    }
    derive instance genericRepTypicalReduxAction :: Rep.Generic
    TypicalJSTaggedObject _
    What about typical JS-style tagged objects?
    typicalReduxActionOptions :: Options
    typicalReduxActionOptions = defaultOptions
    { sumEncoding = TaggedObject
    { tagFieldName: "type"
    , contentsFieldName: "payload" } }
    instance isForeignTypicalReduxAction :: IsForeign TypicalJSTaggedObject where
    read = readGeneric typicalReduxActionOptions
    instance asForeignTypicalReduxAction :: AsForeign TypicalJSTaggedObject where
    write = toForeignGeneric typicalReduxActionOptions

    View full-size slide

  19. Result
    describe "TypicalJSTaggedObject" do
    testJSON
    (Login { username: "agent", password: "hunter2" })
    "{ \"type\": \"Logout\" }"
    (Right (Logout))
    TypicalJSTaggedObject
    ✓ can be converted to JSON
    (Login { password: "hunter2", username: "agent" })
    ->
    {"payload":{"username":"agent","password":"hunter2"},"type":"Login"}
    ✓ can be converted back
    ✓ can be converted from JSON
    { "type": "Logout" }
    -> (Right Logout)

    View full-size slide

  20. I hope I’ve been able to show that
    ● JSON de/serialization can be done largely automatically
    ○ Provided your language gives you the tools to do it
    ● You can manually resolve bits as needed
    ○ Provided your language gives you the tools to do it
    ● You shouldn’t have to do any of this manually in 2017
    ○ Provided your language gives you the tools to do it
    ● You should write some Purescript too
    ○ Or a language of similar power
    ● You should give a similar talk if you’re an F# or OCaml user
    Conclusion

    View full-size slide

  21. Thanks!
    repo: https://github.com/justinwoo/purescript-howto-foreign-generic/

    View full-size slide