【问题标题】:Can aeson handle JSON with imprecise types?aeson 可以处理不精确类型的 JSON 吗?
【发布时间】:2020-12-23 20:24:52
【问题描述】:

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

【问题讨论】:

  • 您是否考虑过编写 aeson 解析器而不是使用派生?这样你就可以完全控制了。您还可以使用自己的 FromJSON 类编写自己的 deriveJSON

标签: json haskell aeson


【解决方案1】:

一个简单的(虽然可能不是那么优雅)的选择是将属性定义为 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",那么已经有一些证据表明您可能不应该相信合同的类型是正确的...

【讨论】:

    【解决方案2】:

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

    {-# 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)
    

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

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


    为了避免这种尴尬并保持我们的域类型干净,我们需要一种方法告诉 GHC:“嘿,当为我的域类型派生 FromJSON 实例时,请将此字段视为 @987654335 @,但最后返回一个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”通过调整Genericrepresentations 起作用,因此该技术不适用于使用Template Haskell 生成的反序列化器。

    【讨论】:

    猜你喜欢
    • 2011-02-16
    • 2018-02-08
    • 1970-01-01
    • 1970-01-01
    • 2019-11-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多