You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何编写FromJSON实例以解析最匹配的VersionedThing版本?

解决VersionedThing的JSON解析歧义问题

你的问题很典型——当多个版本的记录共享必填字段,且其他字段都是可选时,mplus会优先选择第一个能成功解析的版本,导致不符合预期的匹配。要让解析器选择“最正确”的版本,我们需要让每个版本的解析逻辑更具区分度,或者在顶层解析时先判断版本特征。

下面提供两种常用的解决方案,你可以根据自己的需求选择:


方案一:严格检查字段,拒绝多余键

这种方法会让每个版本的解析器验证JSON对象中只有该版本允许的字段,这样当JSON包含其他版本的特有字段时,当前版本的解析会失败,从而让后续版本的解析器有机会运行。

首先,先修正你代码中的一个小问题:Haskell不允许同一模块中有同名的构造函数,所以我们需要把Thing1和Thing2的构造函数改名(比如分别叫Thing1和Thing2):

import Data.Aeson
import Data.Aeson.Types
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)

data VersionedThing = V1 Thing1 | V2 Thing2 deriving (Show)
data Thing1 = Thing1 { name :: Maybe String, val1 :: Maybe String, val2 :: Maybe String } deriving (Show)
data Thing2 = Thing2 { name :: Maybe String, val3 :: Maybe String, val4 :: Maybe String } deriving (Show)

然后编写一个辅助函数,用来检查JSON对象的键是否都在允许的列表中:

-- 验证JSON对象的所有键都属于允许的集合,否则解析失败
allowedKeys :: Set Text -> Object -> Parser ()
allowedKeys allowed obj =
  let actualKeys = Set.fromList $ Map.keys obj
      extraKeys = actualKeys `Set.difference` allowed
  in if Set.null extraKeys
        then return ()
        else fail $ "Unexpected keys for this version: " ++ show extraKeys

接下来修改Thing1和Thing2的FromJSON实例,在解析完成后加上字段检查:

instance FromJSON Thing1 where
    parseJSON (Object v) = do
        thing <- Thing1 <$> v.: "name" <*> v.:? "val1" <*> v.:? "val2"
        -- 只允许name、val1、val2三个键
        allowedKeys (Set.fromList ["name", "val1", "val2"]) v
        return thing
    parseJSON _ = fail "Thing1 expects an Object"

instance FromJSON Thing2 where
    parseJSON (Object v) = do
        thing <- Thing2 <$> v.: "name" <*> v.:? "val3" <*> v.:? "val4"
        -- 只允许name、val3、val4三个键
        allowedKeys (Set.fromList ["name", "val3", "val4"]) v
        return thing
    parseJSON _ = fail "Thing2 expects an Object"

最后,VersionedThing的实例可以保留原来的mplus逻辑,但现在解析行为会符合预期:

instance FromJSON VersionedThing where
    parseJSON v = (V1 <$> parseJSON v) `mplus` (V2 <$> parseJSON v)

比如你提供的JSON:

{ "name":"Foo", "val3":"Bar", "val4":"Baz" }

当尝试解析为Thing1时,会因为存在val3和val4这两个不允许的键而失败,解析器会自动尝试Thing2,最终得到正确的V2 Thing2 (Just "Foo") (Just "Bar") (Just "Baz")。


方案二:通过版本特有字段判断优先级

如果你的场景允许JSON包含额外字段,但希望根据是否存在版本特有的字段来选择解析版本,可以在顶层的VersionedThing解析器中先判断JSON的特征,再选择对应的解析逻辑。

这种方法不需要修改Thing1和Thing2的原解析器,只需要修改VersionedThing的实例:

instance FromJSON VersionedThing where
    parseJSON v@(Object obj) = do
        -- 检查是否存在V1特有的字段(val1或val2)
        let hasV1Fields = any (`Map.member` obj) ["val1", "val2"]
        -- 检查是否存在V2特有的字段(val3或val4)
            hasV2Fields = any (`Map.member` obj) ["val3", "val4"]
        
        case (hasV1Fields, hasV2Fields) of
            -- 只有V1字段:解析为V1
            (True, False) -> V1 <$> parseJSON v
            -- 只有V2字段:解析为V2
            (False, True) -> V2 <$> parseJSON v
            -- 同时存在两种版本的字段:解析失败(可根据需求调整)
            (True, True) -> fail "JSON contains both V1 and V2 specific fields"
            -- 没有任何版本特有字段:可以默认解析为最新版本,或者失败
            (False, False) -> V2 <$> parseJSON v -- 比如默认用V2
    parseJSON _ = fail "VersionedThing expects an Object"

这种方法更灵活,比如如果JSON同时有val1和val3,你可以选择抛出错误,或者根据优先级选择一个版本;如果只有name字段,你可以指定默认解析为最新的版本(比如V2)。


额外建议

  • 如果你的版本迭代比较频繁,建议在JSON中显式加入version字段(比如"version": 1或"version": 2),这样解析器可以直接根据该字段选择对应的版本,完全避免歧义。这是最清晰、最易维护的方案。
  • 注意处理解析失败的情况,比如当JSON同时包含多个版本的特有字段时,应该明确抛出错误,避免产生不确定的结果。

内容的提问来源于stack exchange,提问作者jkeuhlen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 06:22:33