简体   繁体   English

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

[英]Can aeson handle JSON with imprecise types?

I have to deal with JSON from a service that sometimes gives me "123" instead of 123 as the value of field.我必须从有时会给我"123"而不是123作为字段值的服务处理 JSON。 Of course this is ugly, but I cannot change the service.当然这很丑陋,但我不能改变服务。 Is there an easy way to derive an instance of FromJSON that can handle this?有没有一种简单的方法可以派生出可以处理此问题的FromJSON实例? The standard instances derived by means of deriveJSON ( https://hackage.haskell.org/package/aeson-1.5.4.1/docs/Data-Aeson-TH.html ) cannot do that.通过deriveJSON ( https://hackage.haskell.org/package/aeson-1.5.4.1/docs/Data-Aeson-TH.html ) 派生的标准实例无法做到这一点。

One low-hanging (although perhaps not so elegant) option is to define the property as an Aeson Value .一个简单(虽然可能不是那么优雅)的选择是将属性定义为 Aeson Value Here's an example:这是一个例子:

{-#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 can decode a JSON value with a number: Aeson 可以用数字解码 JSON 值:

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

It also works if the value is a string:如果值是字符串,它也有效:

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

Granted, by defining the property as Value this means that at the Haskell side, it could also hold arrays and other objects, so you should at least have a path in your code that handles that.当然,通过将属性定义为Value这意味着在 Haskell 端,它还可以包含 arrays 和其他对象,因此您至少应该在代码中有一个路径来处理它。 If you're absolutely sure that the third-party service will never give you, say, an array in that place, then the above isn't the most elegant solution.如果您绝对确定第三方服务永远不会在那个地方给您一个数组,那么以上并不是最优雅的解决方案。

On the other hand, if it gives you both 123 and "123" , there's already some evidence that maybe you shouldn't trust the contract to be well-typed...另一方面,如果它同时给你123"123" ,那么已经有一些证据表明你不应该相信合同的类型是正确的......

Assuming you want to avoid writing FromJSON instances by hand as much as possible, perhaps you could define a newtype over Int with a hand-crafted FromJSON instance—just for handling that oddly parsed field:假设您想尽可能避免手工编写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)

You could then derive FromJSON for records which have a SpecialInt as a field.然后,您可以为具有SpecialInt作为字段的记录派生FromJSON

Making the field a SpecialInt instead of an Int only for the sake of the FromJSON instance feels a bit intrusive though.只是为了FromJSON实例而将字段设置为SpecialInt而不是Int感觉有点麻烦。 "Needs to be parsed in an odd way" is a property of the external format, not of the domain. “需要以一种奇怪的方式进行解析”是外部格式的属性,而不是域的属性。


In order to avoid this awkwardness and keep our domain types clean, we need a way to tell GHC: "hey, when deriving the FromJSON instance for my domain type, please treat this field as if it were a SpecialInt , but return an Int at the end".为了避免这种尴尬并保持我们的域类型干净,我们需要一种方法来告诉 GHC:“嘿,当为我的域类型派生FromJSON实例时,请将此字段视为一个SpecialInt ,但返回一个Int结束”。 That is, we want to deal with SpecialInt only when deserializing.也就是说,我们只想在反序列化时处理SpecialInt This can be done using the "generic-data-surgery" library.这可以使用“generic-data-surgery”库来完成。

Consider this type考虑这种类型

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

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

and imagine we want to parse "age" as if it were a SpecialInt .并想象我们想要解析“年龄”,就好像它是一个SpecialInt一样。 We can do it like this:我们可以这样做:

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

Putting it to work:开始工作:

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

One limitation is that "generic-data-surgery" works by tweaking Generic representations , so this technique won't work with deserializers generated using Template Haskell .一个限制是“generic-data-surgery”通过调整Generic representations来工作,因此该技术不适用于使用Template Haskell生成的反序列化程序。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM