如何编写FromJSON实例以解析最匹配的VersionedThing版本?
你的问题很典型——当多个版本的记录共享必填字段,且其他字段都是可选时,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

