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

generic combinators backward compatibility issue #961

Open
rpeszek opened this issue Jul 26, 2022 · 2 comments
Open

generic combinators backward compatibility issue #961

rpeszek opened this issue Jul 26, 2022 · 2 comments

Comments

@rpeszek
Copy link

rpeszek commented Jul 26, 2022

IMO is not ideal and makes me question my use of generic combinators in Aeson.
At minimum, this should be documented as a gotcha.

Tested with aeson 1.5.6.0

Start like this

data FooBar = 
    Foo
    | Bar 
    deriving Generic

Things work as expected

import qualified Data.Aeson as A
>>> A.genericToJSON A.defaultOptions Foo 
String "Foo" 

New functionality is requested, new constructor is added, and FooBar becomes

data FooBar = 
    Foo
    | Bar 
    | Baz Int
    deriving Generic
>>> A.genericToJSON A.defaultOptions Foo 
Object (fromList [("tag",String "Foo")])

That IMO is concerning, that breaks compatibility. Adding a new constructor has changed how my old constructors are ToJSON and FromJSON-ed.

There is more (and is possibly related). One would expect allNullaryToStringTag to force the previous behavior (ideally I would not want to do that but let me try)

A.genericToJSON (A.defaultOptions {A.allNullaryToStringTag=True}) Foo
Object (fromList [("tag",String "Foo")])

Nope, what works is:

A.genericToJSON (A.defaultOptions {A.sumEncoding = A.UntaggedValue}) Foo
String "Foo"

Except this will drop tag in the Baz constructor too.
Maybe this has been fixed in newer versions? I could not find a ticket related to this.

@phadej
Copy link
Collaborator

phadej commented Jul 26, 2022

This is "by design". Currently aeson's deriving is "smart" and it figures out a way to encode/decode almost any type. But then such surprises as you describe can happen. That's a motivation to #933 to rather provide newtypes to be used with DerivingVia which are limited in what they accept, but are more stable in encoding they provide.

E.g. if you originally had

deriving via (EnumAsTag FooBar) instance ToJSON FooBar

then adding a new constructor would made above code to not compile, as new FooBar is not an enum anymore.


Separately, I don't know if we'd add a way to encode nullary constructors are encoded as string-tags and non-nullary as tagged records (or some other way they may be encoded), but for now I wouldn't. as current highly configurable options approach is hard to test exhaustively.


Note the docs of allNullaryToStringTag

If True the constructors of a datatype, with all nullary constructors, will be encoded to just a string with the constructor tag. If False the encoding will always follow the sumEncoding.

@rpeszek
Copy link
Author

rpeszek commented Jul 28, 2022

So I missed the emphasized all in the documentation statement. How lame on my part!
I view this as a gotcha, making it louder in the documentation would that be something that we can consider?

I am now considering generic instances in Aeson to be dangerous deriving via is something I never used and will look into using it now even if it is limited.

Thank you for the explanation!

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

2 participants