Skip to content

Instantly share code, notes, and snippets.

@ejconlon
Last active March 6, 2019 16:33
Show Gist options
  • Save ejconlon/30dc50b183a0f72501c3e49ce57cae52 to your computer and use it in GitHub Desktop.
Save ejconlon/30dc50b183a0f72501c3e49ce57cae52 to your computer and use it in GitHub Desktop.
Repro Aeson optional serialization error
#!/usr/bin/env stack
-- stack --resolver lts-13.9 --install-ghc runghc --package aeson --package tasty --package text
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UndecidableInstances #-}
import Data.Aeson
import Data.Aeson.Encoding (encodingToLazyByteString)
import Data.Text (Text)
import Data.Text.Lazy (toStrict)
import Data.Text.Lazy.Encoding (decodeUtf8)
import GHC.Generics (Generic, Generic1, Rep, Rep1)
import Test.Tasty
import Test.Tasty.HUnit
-- Wrapper utils
options :: Options
options = defaultOptions
{ omitNothingFields = True
}
newtype AesonWrapper a = AesonWrapper { unAesonWrapper :: a } deriving (Eq, Show)
instance (Generic a, GToJSON Zero (Rep a), GToEncoding Zero (Rep a)) => ToJSON (AesonWrapper a) where
toJSON = genericToJSON options . unAesonWrapper
toEncoding = genericToEncoding options . unAesonWrapper
newtype AesonWrapper1 f a = AesonWrapper1 { unAesonWrapper1 :: f a } deriving (Eq, Show)
instance (Generic1 f, GToJSON One (Rep1 f), GToEncoding One (Rep1 f)) => ToJSON1 (AesonWrapper1 f) where
liftToJSON a b = genericLiftToJSON options a b . unAesonWrapper1
liftToEncoding a b = genericLiftToEncoding options a b . unAesonWrapper1
-- Now using them
data B = B { required :: Int, optional :: Maybe Int }
deriving (Eq, Show, Generic)
deriving (ToJSON) via (AesonWrapper B)
data C a = C { required :: a, optional :: Maybe a }
deriving (Eq, Show, Generic1)
deriving (ToJSON1) via (AesonWrapper1 C)
expectedWithOptional, expectedWithoutOptional :: Text
expectedWithOptional = "{\"required\":1,\"optional\":2}"
expectedWithoutOptional = "{\"required\":1}"
bWithOptional, bWithoutOptional :: B
bWithOptional = B 1 (Just 2)
bWithoutOptional = B 1 Nothing
cWithOptional, cWithoutOptional :: C Int
cWithOptional = C 1 (Just 2)
cWithoutOptional = C 1 Nothing
encodingToText :: Encoding -> Text
encodingToText = toStrict . decodeUtf8 . encodingToLazyByteString
encodeToText :: ToJSON a => a -> Text
encodeToText = encodingToText . toEncoding
encodeToText1 :: (ToJSON1 f, ToJSON a) => f a -> Text
encodeToText1 = encodingToText . toEncoding1
main :: IO ()
main = defaultMain $
testCase "Example test case" $ do
-- These three cases succeed
encodeToText bWithOptional @?= expectedWithOptional
encodeToText bWithoutOptional @?= expectedWithoutOptional
encodeToText1 cWithOptional @?= expectedWithOptional
-- This one fails:
encodeToText1 cWithoutOptional @?= expectedWithoutOptional
{-
Example test case: FAIL
Repro.hs:77:
expected: "{\"required\":1}"
but got: "{\"required\":1,\"optional\":null}"
-}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment