aeson-schemas: Safely extract JSON data when data types are too cumbersome

aeson-schemas: Safely extract JSON data when data types are too cumbersome

529ce500a0fa87c9a7660f9837fdb651?s=128

Brandon Chinn

August 01, 2020
Tweet

Transcript

  1. aeson-schemas: Safely extract JSON data when data types are too

    cumbersome1 Brandon Chinn 1 August 2020 1 h$p:/ /hackage.haskell.org/package/aeson-schemas
  2. Agenda • Mo$va$on (5 min) • Using aeson-schemas (10 min)

    • Implemen$ng aeson-schemas (10 min) • Type-level programming 101 • Final Thoughts (5 min) Haskell Love 2020 | aeson-schemas 2
  3. Mo#va#on Haskell Love 2020 | aeson-schemas 3

  4. Mo#va#on Parsing data with aeson class ToJSON a where toJSON

    :: a -> Value class FromJSON a where parseJSON :: Value -> Parser a Haskell Love 2020 | aeson-schemas 4
  5. Mo#va#on Parsing data with aeson { "users": [ { "id":

    1, "name": "Alice" }, ... ] } data User = User { id :: Int , name :: String } deriving ( Show , Generic , FromJSON ) Haskell Love 2020 | aeson-schemas 5
  6. Mo#va#on Parsing data with aeson { "name": "Policy1", "permissions": [

    { "resource": { "name": "secretdata.txt", "owner": "john@example.com" }, "access": "READ" } ] } data Result = Result { name :: String , permissions :: [Permission] } data Permission = Permission { resource :: Maybe Resource , access :: String } data Resource = Resource { name :: String , owner :: Maybe String } Haskell Love 2020 | aeson-schemas 6
  7. Mo#va#on Querying a GraphQL API type Query { users: [User!]!

    } type User { id: ID! name: String! posts: [Post!]! } type Post { id: ID! name: String! createdAt: String! } query { users { id name posts { id name } } } Haskell Love 2020 | aeson-schemas 7
  8. Mo#va#on Querying a GraphQL API data Query = Query {

    users :: Maybe [User] } data User = User { id :: Maybe String , name :: Maybe String , posts :: Maybe [Post] } data Post = Post { id :: Maybe ID , name :: Maybe String , createdAt :: Maybe String } • Pros • Direct transla/on of GraphQL schema • Cons • Handle Nothing / use fromJust • id field name shadows Prelude.id • Duplicate name field Haskell Love 2020 | aeson-schemas 8
  9. Mo#va#on Querying a GraphQL API data Query1 = Query1 {

    users :: [User1] } data User1 = User1 { id :: String , name :: String , posts :: [Post1] } data Post1 = Post1 { id :: String , name :: String } • Pros • No more Maybe • Cons • Redefine type per use • Record names s5ll duplicated Haskell Love 2020 | aeson-schemas 9
  10. Problem Requirements 1. Type safe 2. Avoid pollu3ng namespace 3.

    Nice query language Haskell Love 2020 | aeson-schemas 10
  11. Using aeson-schemas Haskell Love 2020 | aeson-schemas 11

  12. Using aeson-schemas import Data.Aeson.Schema (schema) type MySchema = [schema| {

    users: List { id: Int, name: Text, }, } |] import Data.Aeson (decodeFileStrict) import Data.Aeson.Schema (Object, get) obj <- fromJust <$> decodeFileStrict "example.json" :: IO (Object MySchema) -- outputs: -- ["Alice", "Bob", "Claire"] print [get| obj.users[].name |] Haskell Love 2020 | aeson-schemas 12
  13. Using aeson-schemas schema quasiquoter type BasicSchema = [schema| { a:

    Bool, b: Int, c: Double, d: Text, e: UTCTime, } |] type ComplexSchema = [schema| { foo: List { a: Int, b: Maybe Text, }, bar: List Maybe Bool, } |] Haskell Love 2020 | aeson-schemas 13
  14. Using aeson-schemas get quasiquoter let users = [get| obj.a.b.users |]

    map [get| .name |] users -- compare: -- map (fmap c . b) (a obj) [get| obj.a[].b?.c |] Haskell Love 2020 | aeson-schemas 14
  15. Using aeson-schemas GraphQL query query { users { id name

    posts { id name } } } aeson-schemas schema type Query1 = [schema| { users: List { id: Text, name: Text, posts: List { id: Text, name: Text, }, }, } |] Haskell Love 2020 | aeson-schemas 15
  16. Implemen'ng aeson-schemas Haskell Love 2020 | aeson-schemas 16

  17. Type-level programming Value Type Kind2 True, False Bool * Just

    1, Nothing Maybe Int * N/A Maybe * -> * 2 * is actually deprecated in favor of Type from Data.Kind, but I like how * looks be:er, so that's why I'm using it. Haskell Love 2020 | aeson-schemas 17
  18. Type-level programming With -XDataKinds Value Type Kind True, False Bool

    * N/A 'True, 'False Bool Haskell Love 2020 | aeson-schemas 18
  19. Type-level programming Demo: Restaurant.hs Haskell Love 2020 | aeson-schemas 19

  20. Type-level programming Type families type family Foo a where Foo

    Int = [Int] Foo Bool = Maybe Bool x :: Foo Int x = [1, 2, 3] y :: Foo Bool y = Just True Haskell Love 2020 | aeson-schemas 20
  21. Implemen'ng aeson-schemas 1. Define the schema 2. Parse JSON data

    into Object 3. Extract data from Object Haskell Love 2020 | aeson-schemas 21
  22. Implemen'ng aeson-schemas Defining the schema import GHC.TypeLits (Symbol) data SchemaType

    = SchemaInt | SchemaText | SchemaList SchemaType | SchemaObject [(Symbol, SchemaType)] Haskell Love 2020 | aeson-schemas 22
  23. Implemen'ng aeson-schemas Defining the schema {-# LANGUAGE DataKinds #-} type

    MySchema = 'SchemaObject '[ '( "users" , 'SchemaList ( 'SchemaObject '[ '("id", 'SchemaInt) , '("name", 'SchemaText) ] ) ) ] Haskell Love 2020 | aeson-schemas 23
  24. Implemen'ng aeson-schemas Parsing data into Object data Object (schema ::

    SchemaType) = UnsafeObject (HashMap Text Dynamic) instance (IsSchemaType schema, SchemaResult schema ~ Object schema) => FromJSON (Object schema) where parseJSON = parseValue @schema type family SchemaResult (schema :: SchemaType) where SchemaResult 'SchemaInt = Int SchemaResult 'SchemaText = Text SchemaResult ('SchemaList inner) = [SchemaResult inner] SchemaResult ('SchemaObject schema) = Object ('SchemaObject schema) class IsSchemaType (schema :: SchemaType) where parseValue :: Value -> Parser (SchemaResult schema) Haskell Love 2020 | aeson-schemas 24
  25. Implemen'ng aeson-schemas Parsing data into Object instance IsSchemaType 'SchemaInt where

    parseValue = Aeson.parseJSON -- :: Value -> Parser Int instance IsSchemaType 'SchemaText where parseValue = Aeson.parseJSON -- :: Value -> Parser Text instance IsSchemaType inner => IsSchemaType ('SchemaList inner) where parseValue (Aeson.Array a) = traverse (parseValue @inner) (Vector.toList a) parseValue _ = fail "..." Haskell Love 2020 | aeson-schemas 25
  26. Implemen'ng aeson-schemas Parsing data into Object -- ref: SchemaObject [(Symbol,

    SchemaType)] instance (...) => IsSchemaType ('SchemaObject ('(key, inner) ': rest)) where parseValue value@(Aeson.Object o) = do let key = Text.pack $ symbolVal (Proxy @key) inner <- parseValue @inner (HashMap.lookupDefault Aeson.Null key o) UnsafeObject rest <- parseValue @rest value return $ UnsafeObject $ HashMap.insert key (toDyn inner) rest parseValue _ = fail "..." instance IsSchemaType ('SchemaObject '[]) where parseValue (Aeson.Object _) = return $ UnsafeObject HashMap.empty parseValue _ = fail "..." Haskell Love 2020 | aeson-schemas 26
  27. Implemen'ng aeson-schemas Extrac'ng data from Object let o :: Object

    ('SchemaObject '[ '("foo", 'SchemaInt) ]) o = ... getKey @"foo" o :: Int Haskell Love 2020 | aeson-schemas 27
  28. Implemen'ng aeson-schemas Extrac'ng data from Object -- Fcf.Lookup :: a

    -> [(a, b)] -> Fcf.Exp (Maybe b) -- Fcf.FromMaybe :: a -> Maybe a -> Fcf.Exp a -- Fcf.=<< :: (a -> Fcf.Exp b) -> Fcf.Exp a -> Fcf.Exp b -- Fcf.Eval :: Fcf.Exp a -> a type family LookupSchema (key :: Symbol) (schema :: SchemaType) where LookupSchema key ('SchemaObject schemaTypeMap) = Fcf.Eval ( Fcf.FromMaybe ( TypeError ( 'Text "Key '" ':<>: 'Text key ':<>: 'Text "' does not exist in the following schema:" ':$$: 'ShowType schemaTypeMap ) ) =<< Fcf.Lookup key schemaTypeMap ) Haskell Love 2020 | aeson-schemas 28
  29. Implemen'ng aeson-schemas Extrac'ng data from Object getKey :: forall key

    initialSchema. (...) => Object initialSchema -> SchemaResult (LookupSchema key initialSchema) getKey (UnsafeObject o) = fromMaybe (error "This should not happen") $ fromDynamic (o ! Text.pack key) where key = symbolVal (Proxy @key) Haskell Love 2020 | aeson-schemas 29
  30. Implemen'ng aeson-schemas type MySchema = 'SchemaObject '[ '( "users", 'SchemaList

    ( 'SchemaObject '[ '("id", 'SchemaInt), '("name", 'SchemaText) ] ) ) ] o <- fromJust <$> decodeFileStrict "example.json" :: IO (Object MySchema) let names :: [Text] names = map (getKey @"name") $ getKey @"users" o Haskell Love 2020 | aeson-schemas 30
  31. Final Thoughts Haskell Love 2020 | aeson-schemas 31

  32. Thank You h"ps:/ /leapyear.io Haskell Love 2020 | aeson-schemas 32

  33. Q & A Haskell Love 2020 | aeson-schemas 33