Skip to content

Commit

Permalink
Encode Nothing as null rather than undefined (#49)
Browse files Browse the repository at this point in the history
Previously, the Encode (Maybe a) instance was mapping Nothing to
undefined, which meant that Nothing fields in records would disappear
when stringifying:

    -- old behaviour
    newtype Foo = Foo { x :: Maybe Int }
    derive instance genericFoo :: Generic Foo _

    foo = Foo { x: Nothing }
    log (encodeJSON foo)
    -- {"tag":"Foo","contents":{}}

This commit changes the behaviour of the Encode (Maybe a) instance so
that Nothing is mapped to null rather than undefined. This means that
Nothing fields are present in the JSON, with a value of null:

    -- new behaviour
    log (encodeJSON foo)
    -- {"tag":"Foo","contents":{"x":null}}

Note that we remain lenient about decoding records with Maybe fields:
a field may be present and have a value of null, or it may be absent;
in both cases, the field is decoded as Nothing.

This change brings foreign-generic more into line with Aeson, as by
default Aeson will also encode Nothing fields as present with a value of
null (in fact, this is configurable in Aeson: there is a configuration
option `omitNothingFields` which, when turned on, means that
Nothing-valued fields will be omitted during JSON encoding.)

Maybe values which ended up inside arrays in the encoded JSON are
unaffected by this change; previously they were mapped to `null` anyway.

Maybe values at the top level of the encoded JSON, however, are
affected. In fact, encoding Maybe values at the top level could
previously lead to runtime errors:

    -- previously
    > encodeJSON (Nothing :: Maybe Int)
    ./purescript-foreign-generic/.psci_modules/node_modules/Data.Show/foreign.js:30
      var l = s.length;
                ^

    TypeError: Cannot read property 'length' of undefined

    -- now
    > encodeJSON (Nothing :: Maybe Int)
    "null"
  • Loading branch information
hdgarrood authored and paf31 committed Jan 30, 2019
1 parent 429ddba commit 47442ad
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 5 deletions.
6 changes: 3 additions & 3 deletions src/Foreign/Class.purs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Data.Maybe (Maybe, maybe)
import Data.Traversable (sequence)
import Foreign (F, Foreign, ForeignError(..), readArray, readBoolean, readChar, readInt, readNumber, readString, unsafeToForeign)
import Foreign.Internal (readObject)
import Foreign.NullOrUndefined (readNullOrUndefined, undefined)
import Foreign.NullOrUndefined (readNullOrUndefined, null)
import Foreign.Object (Object)
import Foreign.Object as Object

Expand Down Expand Up @@ -116,7 +116,7 @@ instance arrayEncode :: Encode a => Encode (Array a) where
encode = unsafeToForeign <<< map encode

instance maybeEncode :: Encode a => Encode (Maybe a) where
encode = maybe undefined encode
encode = maybe null encode

instance objectEncode :: Encode v => Encode (Object v) where
encode = unsafeToForeign <<< Object.mapWithKey (\_ -> encode)
encode = unsafeToForeign <<< Object.mapWithKey (\_ -> encode)
2 changes: 2 additions & 0 deletions src/Foreign/NullOrUndefined.js
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
exports['null'] = null;

exports['undefined'] = undefined;
2 changes: 2 additions & 0 deletions src/Foreign/NullOrUndefined.purs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ readNullOrUndefined _ value | isNull value || isUndefined value = pure Nothing
readNullOrUndefined f value = Just <$> f value

foreign import undefined :: Foreign

foreign import null :: Foreign
40 changes: 38 additions & 2 deletions test/Main.purs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ import Control.Monad.Except (runExcept)
import Data.Bifunctor (bimap)
import Data.Either (Either(..))
import Data.Generic.Rep (class Generic)
import Data.Maybe (Maybe(..))
import Data.Maybe (Maybe(..), isNothing)
import Data.String (toLower, toUpper)
import Data.Tuple (Tuple(..))
import Effect (Effect)
import Effect.Console (log)
import Foreign.Class (class Encode, class Decode)
import Foreign (isNull, unsafeToForeign)
import Foreign.Class (class Decode, class Encode, decode, encode)
import Foreign.Generic (decodeJSON, defaultOptions, encodeJSON, genericDecodeJSON, genericEncodeJSON)
import Foreign.Generic.Class (class GenericDecode, class GenericEncode)
import Foreign.Generic.EnumEncoding (class GenericDecodeEnum, class GenericEncodeEnum, GenericEnumOptions, genericDecodeEnum, genericEncodeEnum)
import Foreign.Generic.Types (Options)
import Foreign.Index (readProp)
import Foreign.JSON (parseJSON)
import Foreign.Object as Object
import Global.Unsafe (unsafeStringify)
Expand Down Expand Up @@ -91,6 +93,38 @@ testUnaryConstructorLiteral = do
{ constructorTagTransform: f
}

-- Test that `Nothing` record fields, when encoded to JSON, are present and
-- encoded as `null`
testNothingToNull :: Effect Unit
testNothingToNull =
let
json = encode (UndefinedTest {a: Nothing})
in do
log (encodeJSON json)
case runExcept (pure json >>= readProp "contents" >>= readProp "a") of
Right val ->
when (not (isNull val))
(throw ("property 'a' was not null; got: " <> encodeJSON val))
Left err ->
throw (show err)

-- Test that `Maybe` fields which are not present in the JSON are decoded to
-- `Nothing`
testNothingFromMissing :: Effect Unit
testNothingFromMissing =
let
json = unsafeToForeign
{ tag: "UndefinedTest"
, contents: 0
}
in
case runExcept (decode json) of
Right (UndefinedTest x) ->
when (not (isNothing x.a))
(throw ("Expected Nothing, got: " <> show x.a))
Left err ->
throw (show err)

main :: Effect Unit
main = do
testRoundTrip (RecordTest { foo: 1, bar: "test", baz: 'a' })
Expand All @@ -106,3 +140,5 @@ main = do
testUnaryConstructorLiteral
let opts = defaultOptions { fieldTransform = toUpper }
testGenericRoundTrip opts (RecordTest { foo: 1, bar: "test", baz: 'a' })
testNothingToNull
testNothingFromMissing

0 comments on commit 47442ad

Please sign in to comment.