purescript-contrib/purescript-argonaut-codecs

easily encode/decode records

cdepillabout opened this issue · 8 comments

Is there a way to easily encode and decode records?

For instance, I have the following type:

newtype Register = Register { email :: String
                            , password :: String
                            }

I'd like to use purescript-generics for the encoding and decoding like below:

derive instance genericRegister :: Generic Register
instance encodeJsonRegister :: EncodeJson Register where
    encodeJson = gEncodeJson

But when running the following code:

let register = Register { email: "email@email.com", password: "foobar" }
in show $ encodeJson res

I get the following output: {"values":[{"password":"foobar","email":"email@email.com"}],"tag":"Register"}
I was expecting the output to look like this: {"password":"foobar","email":"email@email.com"}

I tried to change the EncodeJson instance like the following:

instance encodeJsonRegister :: EncodeJson Register where
    encodeJson (Register reg) = gEncodeJson reg

But that just gave me the following compiler error:

Error found:
Error in value declaration encodeJsonRegister:
Error at /opt/src/src/Main.purs line 44, column 1 - line 46, column 1:
Error in module Main:
No instance found for

  Data.Generic.Generic { password :: String
                       , email :: String
                       }

The error makes sense, but I was hoping it would be easier to define encodeJson for records. Is there an easy way? I don't want to have to write out something like this for every type:

  instance encodeJsonRegister :: EncodeJson Register where
    encodeJson (Register reg)
      =  "email" := reg.email
      ~> "password" := reg.password
      ~> jsonEmptyObject

Maybe I've just been spoiled by aeson :-(

Perhaps we could add a special case in gEncodeJSON/gDecodeJSON for ADTs with single constructors and single argument, so that they would be simply encoding/decoding that argument.

How does that sound?

At the moment gEncodeJson/gDecodeJson is just a direct, unambiguous, and very obvious (though a bit too much structured) mapping between GenericSpine and JSON.

To be honest, I didn't really intend it to be used as something "human consumable", the primary target are the cases where you just need an isomorphic serialization/deserialization (like if you want to save something to localstorage).

Making an exception for newtypes would break that property, and gDecodeJSON would become ambiguous.

For example if you have:

newtype Natural = Natural Int

You probably don't want gDecodeJson "-55" :: Either String Natural to return you Right (Natural (-55)).

I guess I'll just write my own version of gEncodeJSON.

It would be nice to have a function similar to deriveJSON that could take options on how to do the encoding/decoding.

Then you would be able to write something like this:

myOpts :: ArgonautCodecOptions
myOpts ...

derive instance genericRegister :: Generic Register
instance encodeJsonRegister :: EncodeJson Register where
    encodeJson = encodeJsonWithOptions myOpts
instance decodeJsonRegister :: DecodeJson Register where
    decodeJson = decodeJsonWithOptions myOpts

I am currently working on genericEncodeJson which takes options like Haskell's aeson does. I plan to file a pull request later in the evening. Unwrapping newtype like data is currently not supported, but it should be easy to add.

Ok, after further investigation, this modified example from @cdepillabout does not work:

newtype Register = Register RegisterImpl

type RegisterImpl = { email :: String
                    , password :: String
                    }

-- instance genericRegisterImpl :: Generic RegisterImpl
instance encodeJsonRegister :: EncodeJson Register where
    encodeJson (Register reg) = gEncodeJson reg

Nor does it work, if you add:

instance genericRegisterImpl :: Generic RegisterImpl

or

instance genericRegisterImpl :: Generic Object ( email :: String , password :: String )

this seems to be related to:

purescript/purescript#1822

I was not aware of this problem, ok I think I have to implement the "unwrapUnaryRecords" option.

unwrapUnaryRecords implemented here.

Can this be closed? It doesn't serve the use case of a type having all *code-able non-Generic fields however.

Yes, I believe this can be considered fixed with the unwrapUnaryRecords option.

Thanks!