fsprojects / Fleece

Json mapper for F#
http://fsprojects.github.io/Fleece
Apache License 2.0
199 stars 31 forks source link

Deserializing null values to Option #87

Open pnobre opened 4 years ago

pnobre commented 4 years ago

Trying to deserialize a null value into an option produces an obj expected value (json type mismatch). The behavior should be consistent whether the value is null or doesn't exist, but that's not the case. It correctly returns a None when the value is absent, and and error when the value is present, but set to null.

Repro

type Address = { 
    country: string }
with 
    static member OfJson json = 
        match json with 
        | JObject o -> monad { 
            let! country = o .@ "country"

            return { country = country }}
        | x -> Decode.Fail.objExpected x

type Customer = { 
    id: int 
    name: string
    address: Address option} 
with 
    static member OfJson json = 
        match json with 
        | JObject o -> monad { 
            let! id = o .@ "id"
            let! name = o .@ "name"
            let! address = o .@? "address"
            return { 
                id = id
                name = name
                address = address } }
        | x -> Decode.Fail.objExpected x

Succeds - Returns Some { id = 1; name = "Joe"; address = None}

let json = """{"id":1, "name": "Joe"}"""
let (customer:Customer ParseResult) = parseJson json

Fails - Should return Some { id = 1; name = "Joe"; address = None}

let json' = """{"id":1, "name": "Joe", "address": null}"""
let (customer':Customer ParseResult) = parseJson json 

a potential solution for this issue would be to modify jgetOptionWith function like:

let inline jgetOptWith ofJson (o: IReadOnlyDictionary<string, JsonValue>) key =
        match o.TryGetValue key with
        | true, JNull -> Success None
        | true, value -> ofJson value |> map Some
        | _ -> Success None

//edited to update the test cases and confirm the solution works for other libraries as well.

pnobre commented 4 years ago

just an update, just tried aeson, and what I'm reporting here is aeson's behavior

{-# LANGUAGE OverloadedStrings #-}
import Control.Applicative
import Data.Aeson
import Data.Text (Text)
data Customer = Customer
                { id      :: Integer
                , name    :: String
                , address :: Maybe Address }
                deriving Show
data Address = Address
               { country :: String }
               deriving Show
instance FromJSON Address where 
    parseJSON (Object v) = Address <$> v .: "country"
    parseJSON _ = empty
instance FromJSON Customer where 
    parseJSON (Object v) = Customer <$> v .: "id" <*> v .: "name" <*> v .:? "address"
    parseJSON _ = empty
instance ToJSON Address where 
    toJSON (Address country) = object ["country" .= country]
instance ToJSON Customer where 
    toJSON (Customer id name address) = object ["id" .= id, "name" .= name, "address" .= address]
pnobre commented 4 years ago

from what I see here, the problem is when some folks might actually want to get nulls back.

//edit actually, giving a better though on this, there's no way someone would actually get nulls. .@? will return a None if the value isn't present, but it'll still fail for most cases (unless there's a particular OfJson implementation) when the json value is null.

gusty commented 2 years ago

Note that as from v 0.10.0 jopt and friends, support Nullables, ValueOption and anything that has the zero value (even lists).

Not sure if this solves your specific problem, I think if they supply nulls and the field is Nullable it should work now. Otherwise please let me know.