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

3

我需要处理一项服务的JSON,该服务有时会将字段的值 "123" 而不是 123 作为返回结果。虽然这很丑陋,但我无法更改该服务。有没有一种简单的方法来推导出可以处理它的 FromJSON 实例?使用 deriveJSON 派生的标准实例(https://hackage.haskell.org/package/aeson-1.5.4.1/docs/Data-Aeson-TH.html)无法解决这个问题。


1
你有没有考虑使用aeson解析器而不是使用派生呢?这样就可以完全控制了。你也可以编写自己的deriveJSONFromJSON类。 - Li-yao Xia
2个回答

3

一个简单(虽然可能不太优雅)的选择是将属性定义为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"两种形式,那么这已经表明您可能不能信任合同的类型安全性...


1
假设您希望尽可能避免手动编写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)

你可以为包含SpecialInt字段的记录派生FromJSON
但仅出于FromJSON实例而将字段设置为SpecialInt似乎有些强行。"需要以奇怪的方式解析"是外部格式的属性,而不是领域的属性。
为了避免这种尴尬并保持我们的域类型清晰,我们需要一种告诉GHC的方法:“嘿,在为我的域类型派生FromJSON实例时,请将此字段视为SpecialInt,但最终返回一个Int”。也就是说,我们只想在反序列化时处理SpecialInt。可以使用"generic-data-surgery"库来完成这项工作。
考虑以下类型
{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics

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

假设我们想将“age”解析为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表示来实现,因此该技术无法与使用Template Haskell生成的反序列化器配合使用。

显然,这种修改也可以使用“generic-lens”进行操作。http://hackage.haskell.org/package/generic-data-0.8.3.0/docs/Generic-Data-Microsurgery.html#g:1 - danidiaz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接