简体   繁体   English

如何在 Haskell 中解析带有可选和变体类型字段的 json?

[英]how to parse json with field of optional and variant type in Haskell?

How I can parse the input json inside this file ?我如何解析这个文件中的输入 json ? https://github.com/smogon/pokemon-showdown/blob/master/data/moves.js https://github.com/smogon/pokemon-showdown/blob/master/data/moves.js

For the secondary and flags properties?对于次要和标志属性? They are optional and contains variant type.它们是可选的并且包含变体类型。

A minimal example would be this one:一个最小的例子是这个:

[
  {},
  {
    "secondary": false
  },
  {

    "secondary": {
      "chance": 10,
      "boosts": {
        "spd": -1
      }
    }
  },
  {
    "secondary": {
      "chance": 30,
      "volatileStatus": "flinch"
    }
  },
  {
    "secondary": {
      "chance": 30
    }
  },
  {
    "secondary": {
      "chance": 10,
      "self": {
        "boosts": {
          "atk": 1,
          "def": 1,
          "spa": 1,
          "spd": 1,
          "spe": 1
        }
      }
    }
  },
  {
    "secondary": {
      "chance": 10,
      "status": "brn"
    }
  },
  {
    "secondary": {
      "chance": 50,
      "self": {
        "boosts": {
          "def": 2
        }
      }
    }
  },
  {
    "secondary": {
      "chance": 100,
      "self": {}
    }
  },
  {
    "secondary": {
      "chance": 50,
      "boosts": {
        "accuracy": -1
      }
    }
  }
]

For your convenience, you can choose to attach this snippet to the end of the js file and run it using node move.js .为方便起见,您可以选择将此代码段附加到 js 文件的末尾并使用node move.js运行它。 Two valid json files will be saved to your disk.两个有效的 json 文件将保存到您的磁盘。 One is a list of json objects while the other is an object with string as key.一个是 json 对象列表,而另一个是以字符串为键的对象。


var fs = require('fs');
fs.writeFile("moves_object.json", JSON.stringify(BattleMovedex), function(err) {}); // 1. save a json object with string key

var jsonList = []
for (var key of Object.keys(BattleMovedex)) {
    jsonList.push(BattleMovedex[key]);
}
fs.writeFile("moves.json", JSON.stringify(jsonList), function(err) { // 2. save as a list of json object
    if (err) {
        console.log(err);
    }
});

FYI : If you are familiar with c++, you might find it easier to understand the same problem in this post:仅供参考:如果您熟悉 C++,您可能会发现在这篇文章中更容易理解相同的问题:

How to parse json file with std::optional< std::variant > type in C++? 如何使用 C++ 中的 std::optional< std::variant > 类型解析 json 文件?

NOTE : In the code examples below, I've used a "moves.json" file whose contents are your minimal example above.注意:在下面的代码示例中,我使用了一个"moves.json"文件,其内容是上面的最小示例。 Except for getMoves , which can parse any valid JSON, the other code examples won't work on the "moves.json" file derived from the linked "moves.js" file because the format is different (eg, it's an object, not an array, for one thing).除了可以解析任何有效 JSON 的getMoves之外,其他代码示例不适用于从链接的"moves.json"文件派生的"moves.js"文件,因为格式不同(例如,它是一个对象,而不是一个数组,一方面)。

The simplest way of using Aeson to parse arbitrary JSON is to parse it to a Value :使用 Aeson 解析任意 JSON 的最简单方法是将其解析为Value

import Data.Aeson
import Data.Maybe
import qualified Data.ByteString.Lazy as B

getMoves :: IO Value
getMoves = do
  mv <- decode <$> B.readFile "moves.json"
  case mv of
    Nothing -> error "invalid JSON"
    Just v -> return v

Any valid JSON can be parsed this way, and the resulting Value has completely dynamic structure that can be programmatically inspected at runtime.任何有效的 JSON 都可以通过这种方式解析,并且生成的Value具有完全动态的结构,可以在运行时以编程方式检查。 The Lens library and Maybe monad can be helpful here. Lens 库和Maybe monad 在这里会有所帮助。 For example, to find the (first) object with a non-missing secondary.chance of 100, you could use:例如,要找到非缺失的secondary.chance为 100 的(第一个)对象,您可以使用:

{-# LANGUAGE OverloadedStrings #-}

import Control.Lens
import Data.Aeson
import Data.Aeson.Lens
import qualified Data.Vector as Vector
import qualified Data.ByteString.Lazy as B

find100 :: Value -> Maybe Value
find100 inp = do
  arr <- inp ^? _Array
  Vector.find (\s -> s ^? key "secondary" . key "chance" . _Integer == Just 100) arr

test1 = find100 <$> getMoves

which outputs:输出:

> test1
Just (Object (fromList [("secondary",Object (fromList [("chance",Number 100.0),
("self",Object (fromList []))]))]))

which is the Value representation of the object:这是对象的Value表示:

{
  "secondary": {
    "chance": 100,
    "self": {}
  }
}

If you want the resulting parsed object to have more structure, then you need to start by figuring out a Haskell representation that will work with all possible objects you're planning to parse.如果您希望生成的解析对象具有更多结构,那么您需要首先找出一个 Haskell 表示,该表示将适用于您计划解析的所有可能对象。 For your example, a reasonable representation might be:对于您的示例,合理的表示可能是:

type Moves = [Move]

data Move = Move
  { secondary :: Secondary'
  } deriving (Show, Generic)

newtype Secondary' = Secondary' (Maybe Secondary) -- Nothing if json is "false"
  deriving (Show, Generic)

data Secondary = Secondary
  { chance :: Maybe Int
  , boosts :: Maybe Boosts
  , volatileStatus :: Maybe String
  , self :: Maybe Self
  } deriving (Show, Generic)

data Self = Self
  { boosts :: Maybe Boosts
  } deriving (Show, Generic)

newtype Boosts = Boosts (HashMap.HashMap Text.Text Int)
  deriving (Show, Generic)

This assumes that all moves have a secondary field which is either "false" or an object.这假设所有移动都有一个secondary字段,它要么是"false"要么是一个对象。 It also assumes that lots of boost keys are possible, so it's more convenient to represent them as arbitrary text strings in a Boosts hashmap.它还假设有很多 boost 键是可能的,因此在Boosts哈希图中将它们表示为任意文本字符串会更方便。 Also, this handles having the "boosts" directly under "secondary" or nested within "self" , since your example included examples of both forms, though maybe this was a mistake.此外,这可以处理直接在"secondary"或嵌套在"self"中的"boosts" ,因为您的示例包含两种形式的示例,尽管这可能是一个错误。

For these data types, the default instances for Move , Self , and Secondary can all be used:对于这些数据类型,都可以使用MoveSelfSecondary的默认实例:

instance FromJSON Move
instance FromJSON Self
instance FromJSON Secondary

The Secondary' newtype wrapper around Secondary is then used to handle false versus an object using a custom instance:然后使用Secondary周围的Secondary' newtype 包装器来处理false与使用自定义实例的对象:

instance FromJSON Secondary' where
  parseJSON (Bool False) = pure $ Secondary' Nothing
  parseJSON o = Secondary' . Just <$> parseJSON o

A custom instance is also needed for Boosts to parse it into the appropriate hashmap: Boosts还需要一个自定义实例将其解析为适当的哈希图:

instance FromJSON Boosts where
  parseJSON = withObject "Boosts" $ \o -> Boosts <$> mapM parseJSON o

Now, with the following driver:现在,使用以下驱动程序:

test2 :: IO (Either String Moves)
test2 = eitherDecode <$> B.readFile "moves.json"

this decodes your example like so:这会像这样解码您的示例:

> test2
Right [Move {secondary = Secondary' Nothing},Move {secondary =
Secondary' (Just (Secondary {chance = Just 10, boosts = Just (Boosts
(fromList [("spd",-1)])), volatileStatus = Nothing, self =
...

By using eitherDecode above, we can get an error message if the parse fails.通过使用上面的eitherDecode ,如果解析失败,我们可以获得错误消息。 For example, if you run this on the "moves.json" derived from "moves.js" instead, you get:例如,如果你在运行此"moves.json"源自"moves.js" ,而不是,您可以:

> test2
Left "Error in $: parsing [] failed, expected Array, but encountered Object"

when the parser notices that it's trying to parse a [Move] array but is instead finding an object keyed by Pokemon move names.当解析器注意到它正在尝试解析一个[Move]数组,但却在寻找一个由 Pokemon 移动名称键控的对象时。

Here's the full code showing both types of parsing:这是显示两种解析类型的完整代码:

{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}

import Control.Lens
import Data.Aeson
import Data.Aeson.Lens
import GHC.Generics
import qualified Data.Text as Text
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Vector as Vector
import qualified Data.ByteString.Lazy as B

--
-- Parse into a dynamic Value representation

getMoves :: IO Value
getMoves = do
  mv <- decode <$> B.readFile "moves.json"
  case mv of
    Nothing -> error "invalid JSON"
    Just v -> return v

find100 :: Value -> Maybe Value
find100 inp = do
  arr <- inp ^? _Array
  Vector.find (\s -> s ^? key "secondary" . key "chance" . _Integer == Just 100) arr

test1 :: IO (Maybe Value)
test1 = find100 <$> getMoves

--
-- Parse into suitable static data structures

-- whole file is array of moves
type Moves = [Move]

data Move = Move
  { secondary :: Secondary'
  } deriving (Show, Generic)

newtype Secondary' = Secondary' (Maybe Secondary) -- Nothing if json is "false"
  deriving (Show, Generic)

data Secondary = Secondary
  { chance :: Maybe Int
  , boosts :: Maybe Boosts
  , volatileStatus :: Maybe String
  , self :: Maybe Self
  } deriving (Show, Generic)

data Self = Self
  { boosts :: Maybe Boosts
  } deriving (Show, Generic)

newtype Boosts = Boosts (HashMap.HashMap Text.Text Int)
  deriving (Show, Generic)

instance FromJSON Move
instance FromJSON Self
instance FromJSON Secondary

instance FromJSON Secondary' where
  parseJSON (Bool False) = pure $ Secondary' Nothing
  parseJSON o = Secondary' . Just <$> parseJSON o

instance FromJSON Boosts where
  parseJSON = withObject "Boosts" $ \o -> Boosts <$> mapM parseJSON o

test2 :: IO (Either String Moves)
test2 = eitherDecode <$> B.readFile "moves.json"

My attempt to the minimal sample我对最小样本的尝试

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}


module Main where

import Data.Text
import GHC.Generics
import Data.Aeson

main :: IO ()
main = do
  result <- eitherDecodeFileStrict "/tmp/helloworld/minimal.json"
  print (result :: Either String [Foo])

data Foo = Foo { secondary :: Either Bool Bar } deriving (Generic, Show)
data Bar = Chance
  { chance :: Int
  , volatileStatus :: Maybe Text
  , boosts :: Maybe Boosts
  , self :: Maybe Self
  , status :: Maybe Text
  } deriving (Generic, Show)

data Boosts = Boosts
  { atk :: Maybe Int
  , def :: Maybe Int
  , spa :: Maybe Int
  , spd :: Maybe Int
  , spe :: Maybe Int
  } deriving (Generic, Show)

data Self = Self
  { boosts :: Maybe Boosts
  } deriving (Generic, Show)

instance FromJSON Foo where
  parseJSON (Object v) = do
    sd <- v .: "secondary"  -- Parse Value
    case sd of
      Bool x -> return . Foo . Left $ x
      otherwise -> (Foo . Right) <$> parseJSON sd
instance FromJSON Bar
instance FromJSON Boosts
instance FromJSON Self

here is another attempt to your mover.json这是对您的mover.json另一种尝试

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}


module Main where

import Control.Applicative
import Data.Maybe
import Data.Text (Text)
import GHC.Generics
import Data.Aeson

main :: IO ()
main = do
  result <- eitherDecodeFileStrict "/tmp/helloworld/movers.json" 
  case ( result :: Either String [Move]) of
    Left error -> print error
    Right ms -> print (length ms)

data Move = Move
  { num :: Int
  , accuracy :: Either Int Bool
  , secondary :: Maybe (Either Bool Secondary)
  } deriving (Generic, Show)

data Secondary = Secondary
  { chance :: Maybe Int
  , volatileStatus :: Maybe Text
  , boosts :: Maybe Boosts
  , self :: Maybe Self
  , status :: Maybe Text
  } deriving (Generic, Show)

data Boosts = Boosts
  { atk :: Maybe Int
  , def :: Maybe Int
  , spa :: Maybe Int
  , spd :: Maybe Int
  , spe :: Maybe Int
  } deriving (Generic, Show)

data Self = Self
  { boosts :: Maybe Boosts
  } deriving (Generic, Show)

instance FromJSON Move where
  parseJSON (Object v) = Move
    <$> v .: "num"
    <*> (   (Left  <$> v .: "accuracy")
        <|> (Right <$> v .: "accuracy")
        )
    <*> (   fmap (fmap Left)  (v .:? "secondary")
        <|> fmap (fmap Right) (v .:? "secondary")
        )

instance FromJSON Secondary
instance FromJSON Boosts
instance FromJSON Self

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

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