aeson 可以处理类型不精确的 JSON 吗?

问题描述

我必须处理来自有时给我 "123" 而不是 123 作为字段值的服务的 JSON。当然这很丑陋,但我无法更改服务。是否有一种简单的方法可以派生出可以处理此问题的 FromJSON 实例?通过 deriveJSON (https://hackage.haskell.org/package/aeson-1.5.4.1/docs/Data-Aeson-TH.html) 派生的标准实例无法做到这一点。

解决方法

一个低调(虽然可能不是那么优雅)的选择是将属性定义为 Aeson Value。举个例子:

{-#LANGUAGE DeriveGeneric #-}
module Q65410397 where

import GHC.Generics
import Data.Aeson

data JExample = JExample { jproperty :: Value } deriving (Eq,Show,Generic)

instance ToJSON JExample where

instance FromJSON JExample where

Aeson 可以用数字解码 JSON 值:

*Q65410397> decode "{\"jproperty\":123}" :: Maybe JExample
Just (JExample {jproperty = Number 123.0})

如果值是字符串,它也有效:

*Q65410397> decode "{\"jproperty\":\"123\"}" :: Maybe JExample
Just (JExample {jproperty = String "123"})

当然,通过将属性定义为 Value 这意味着在 Haskell 端,它也可以保存数组和其他对象,因此您的代码中至少应该有一个路径来处理它。如果您绝对确定第三方服务永远不会在该位置为您提供数组,那么上述解决方案并不是最优雅的解决方案。

另一方面,如果它同时为您提供 123"123",那么已经有一些证据表明您可能不应该相信合同的类型是正确的...

,

假设您想尽可能避免手动编写 FromJSON 实例,也许您可​​以使用手工制作的 Int 实例在 FromJSON 上定义新类型——只是为了处理它奇怪的解析字段:

{-# LANGUAGE TypeApplications #-}
import Control.Applicative
import Data.Aeson
import Data.Text
import Data.Text.Read (decimal)

newtype SpecialInt = SpecialInt { getSpecialInt :: Int } deriving (Show,Eq,Ord)

instance FromJSON SpecialInt where
  parseJSON v =
    let fromInt = parseJSON @Int v
        fromStr = do
          str <- parseJSON @Text v
          case decimal str of
            Right (i,_) -> pure i
            Left errmsg -> fail errmsg
     in SpecialInt <$> (fromInt <|> fromStr)

然后,您可以为具有 FromJSON 作为字段的记录派生 SpecialInt

仅仅为了 SpecialInt 实例而将字段设为 Int 而不是 FromJSON 感觉有点侵入性。 “需要以奇怪的方式解析”是外部格式的属性,而不是域的属性。


为了避免这种尴尬并保持我们的域类型干净,我们需要一种方法来告诉 GHC:“嘿,当为我的域类型派生 FromJSON 实例时,请将此字段视为一个SpecialInt,但最后返回一个 Int”。也就是说,我们只想在反序列化时处理 SpecialInt。这可以使用 "generic-data-surgery" 库来完成。

考虑这种类型

{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics

data User = User { name :: String,age :: Int } deriving (Show,Generic)

假设我们想解析“年龄”,就好像它是一个 SpecialInt。我们可以这样做:

{-# LANGUAGE DataKinds #-}
import Generic.Data.Surgery (toOR',modifyRField,fromOR,Data)

instance FromJSON User where
  parseJSON v = do
    r <- genericParseJSON defaultOptions v
    -- r is a synthetic Data which we must tweak in the OR and convert to User
    let surgery = fromOR . modifyRField @"age" @1 getSpecialInt . toOR'
    pure (surgery r)

开始工作:

{-# LANGUAGE OverloadedStrings #-}
main :: IO ()
main = do 
    print $ eitherDecode' @User $ "{ \"name\" : \"John\",\"age\" : \"123\" }"
    print $ eitherDecode' @User $ "{ \"name\" : \"John\",\"age\" : 123 }"

一个限制是“generic-data-surgery”通过调整 Generic representations 起作用,因此该技术不适用于使用 Template Haskell 生成的反序列化器。