:deciduous_tree: Crossing the road between Haskell and Elm
Haskell
92
72 commits
updated Feb 28, 2026

Crossing the road between Haskell and Elm.
elm-street allows you to automatically generate definitions of Elm data types and compatible JSON encoders and decoders
from Haskell types. This helps to avoid writing and maintaining huge chunk of boilerplate code when developing full-stack
applications.
In order to use elm-street features, you need to perform the following steps:
Add elm-street to the dependencies of your Haskell package.
Derive the Elm typeclass for relevant data types. You also need to derive
JSON instances according to elm-street naming scheme.
This can be done like this:
import Elm (Elm, elmStreetParseJson, elmStreetToJson)
data User = User
{ userName :: Text
, userAge :: Int
} deriving (Generic)
deriving anyclass (Elm)
instance ToJSON User where toJSON = elmStreetToJson
instance FromJSON User where parseJSON = elmStreetParseJson
NOTE: This requires extensions
-XDerivingStrategies,-XDeriveGeneric,-XDeriveAnyClass.
Alternatively you can use -XDerivingVia to remove some boilerplate (available since GHC 8.6.1):
import Elm (Elm, ElmStreet (..))
data User = User
{ userName :: Text
, userAge :: Int
} deriving (Generic)
deriving (Elm, ToJSON, FromJSON) via ElmStreet User
Create list of all types you want to expose to Elm:
type Types =
'[ User
, Status
]
NOTE: This requires extension
-XDataKinds.
Use generateElm function to output definitions to specified directory under
specified module prefix.
main :: IO ()
main = generateElm @Types $ defaultSettings "frontend/src" ["Core", "Generated"]
NOTE: This requires extension
-XTypeApplications.
When executed, the above program generates the following files:
frontend/src/Core/Generated/Types.elm: Core.Generated.Types module with the definitions for the types, as well as show*, read*, un*, and universe* functions as specified in src/Elm/Print/Types.hsfrontend/src/Core/Generated/Encoder.elm: Core.Generated.Encoder module with the JSON encoders for the typesfrontend/src/Core/Generated/Decoder.elm: Core.Generated.Decoder module with the JSON decoders for the typesfrontend/src/Core/Generated/ElmStreet.elm: Core.Generated.ElmStreet module with bundled helper functionsIf you want to use capabilities provided by elm-street in your Elm
application, you need to have several Elm packages preinstalled in the project. You
can install them with the following commands:
elm install elm/time
elm install elm/json
elm install NoRedInk/elm-json-decode-pipeline
elm install rtfeldman/elm-iso8601-date-strings
Elm-street is not trying to be as general as possible and support every
use-case. The library is opinionated in some decisions and contains several
limitations, specifically:
data UserStatus = UserStatus
{ userStatusId :: Id
, userStatusRemarks :: Text
}
data HealthReading = HealthReading
{ hrUser :: User
, hrDate :: UTCTime
, hrWeight :: Double
}
Elm instance which
will generate valid Elm defintions. Here is how you can create Elm instance for
newtypes with phantom type variables:
newtype Id a = Id { unId :: Text }
instance Elm (Id a) where
toElmDefinition _ = elmNewtype @Text "Id" "unId"
-- - Not supported
data Address
= Post { postCode :: Text }
| Full { fullStreet :: Text, fullHouse :: Int }
-- - Not supported
data Foo
= Bar Int Text
| Baz Int Int Text Text Double Double Bool Bool Char
elm-street generates type alias for
record data type. So the generated Elm type for the following Haskell data
type won't compile in Elm:
data User = User
{ userName :: Text
, userFollowers :: [User]
}
ToJSON/FromJSON instances from the aeson library except you need
to strip record field prefixes. Fortunately, this also can be done
generically. You can use functions from Elm.Aeson module to derive JSON
instances from the aeson package.UTCTime Haskell data type is supported and it's translated to Posix
type in Elm.ifthenelsecaseofletintypemodulewhereimportexposingasporttag (reserved for constructor name due to aeson options)FromJSON and ToJSON instances should be derived using newtype strategy. And Elm should be derived using anyclass strategy:
newtype Newtype = Newtype Int
deriving newtype (FromJSON, ToJSON)
deriving anyclass (Elm)
The frontend directory contains example of minimal Elm project that shows how
generated types are used. To play with this project, do:
generate-elm binary:
cabal new-run generate-elm
cabal new-run run-backend
frontend folder:
cd frontend
elm-app start
Below you can see some examples of how Haskell data types are converted to Elm
types with JSON encoders and decoders using the elm-street library.
Haskell
data User = User
{ userName :: Text
, userAge :: Int
} deriving (Generic)
deriving (Elm, ToJSON, FromJSON) via ElmStreet User
Elm
type alias User =
{ name : String
, age : Int
}
encodeUser : User -> Value
encodeUser x = E.object
[ ("name", E.string x.name)
, ("age", E.int x.age)
]
decodeUser : Decoder User
decodeUser = D.succeed User
|> required "name" D.string
|> required "age" D.int
Haskell
data RequestStatus
= Approved
| Rejected
| Reviewing
deriving (Generic)
deriving (Elm, ToJSON, FromJSON) via ElmStreet RequestStatus
Elm
type RequestStatus
= Approved
| Rejected
| Reviewing
showRequestStatus : RequestStatus -> String
showRequestStatus x = case x of
Approved -> "Approved"
Rejected -> "Rejected"
Reviewing -> "Reviewing"
readRequestStatus : String -> Maybe RequestStatus
readRequestStatus x = case x of
"Approved" -> Just Approved
"Rejected" -> Just Rejected
"Reviewing" -> Just Reviewing
_ -> Nothing
universeRequestStatus : List RequestStatus
universeRequestStatus = [Approved, Rejected, Reviewing]
encodeRequestStatus : RequestStatus -> Value
encodeRequestStatus = E.string << showRequestStatus
decodeRequestStatus : Decoder RequestStatus
decodeRequestStatus = elmStreetDecodeEnum readRequestStatus
Haskell
newtype Age = Age
{ unAge :: Int
} deriving (Generic)
deriving newtype (FromJSON, ToJSON)
deriving anyclass (Elm)
Elm
type alias Age =
{ age : Int
}
encodeAge : Age -> Value
encodeAge x = E.int x.age
decodeAge : Decoder Age
decodeAge = D.map Age D.int
Haskell
newtype Id a = Id
{ unId :: Text
} deriving (Generic)
deriving newtype (FromJSON, ToJSON)
instance Elm (Id a) where
toElmDefinition _ = elmNewtype @Text "Id" "unId"
Elm
type alias Id =
{ unId : String
}
encodeId : Id -> Value
encodeId x = E.string x.unId
decodeId : Decoder Id
decodeId = D.map Id D.string
Haskell
data Guest
= Regular Text Int
| Visitor Text
| Blocked
deriving (Generic)
deriving (Elm, ToJSON, FromJSON) via ElmStreet Guest
Elm
type Guest
= Regular String Int
| Visitor String
| Blocked
encodeGuest : Guest -> Value
encodeGuest x = E.object <| case x of
Regular x1 x2 -> [("tag", E.string "Regular"), ("contents", E.list identity [E.string x1, E.int x2])]
Visitor x1 -> [("tag", E.string "Visitor"), ("contents", E.string x1)]
Blocked -> [("tag", E.string "Blocked"), ("contents", E.list identity [])]
decodeGuest : Decoder Guest
decodeGuest =
let decide : String -> Decoder Guest
decide x = case x of
"Regular" -> D.field "contents" <| D.map2 Regular (D.index 0 D.string) (D.index 1 D.int)
"Visitor" -> D.field "contents" <| D.map Visitor D.string
"Blocked" -> D.succeed Blocked
c -> D.fail <| "Guest doesn't have such constructor: " ++ c
in D.andThen decide (D.field "tag" D.string)
Haskell
74.6%
Elm
20.3%
JavaScript
4.2%
:deciduous_tree: Crossing the road between Haskell and Elm
Haskell
92
72 commits
updated Feb 28, 2026

Crossing the road between Haskell and Elm.
elm-street allows you to automatically generate definitions of Elm data types and compatible JSON encoders and decoders
from Haskell types. This helps to avoid writing and maintaining huge chunk of boilerplate code when developing full-stack
applications.
In order to use elm-street features, you need to perform the following steps:
Add elm-street to the dependencies of your Haskell package.
Derive the Elm typeclass for relevant data types. You also need to derive
JSON instances according to elm-street naming scheme.
This can be done like this:
import Elm (Elm, elmStreetParseJson, elmStreetToJson)
data User = User
{ userName :: Text
, userAge :: Int
} deriving (Generic)
deriving anyclass (Elm)
instance ToJSON User where toJSON = elmStreetToJson
instance FromJSON User where parseJSON = elmStreetParseJson
NOTE: This requires extensions
-XDerivingStrategies,-XDeriveGeneric,-XDeriveAnyClass.
Alternatively you can use -XDerivingVia to remove some boilerplate (available since GHC 8.6.1):
import Elm (Elm, ElmStreet (..))
data User = User
{ userName :: Text
, userAge :: Int
} deriving (Generic)
deriving (Elm, ToJSON, FromJSON) via ElmStreet User
Create list of all types you want to expose to Elm:
type Types =
'[ User
, Status
]
NOTE: This requires extension
-XDataKinds.
Use generateElm function to output definitions to specified directory under
specified module prefix.
main :: IO ()
main = generateElm @Types $ defaultSettings "frontend/src" ["Core", "Generated"]
NOTE: This requires extension
-XTypeApplications.
When executed, the above program generates the following files:
frontend/src/Core/Generated/Types.elm: Core.Generated.Types module with the definitions for the types, as well as show*, read*, un*, and universe* functions as specified in src/Elm/Print/Types.hsfrontend/src/Core/Generated/Encoder.elm: Core.Generated.Encoder module with the JSON encoders for the typesfrontend/src/Core/Generated/Decoder.elm: Core.Generated.Decoder module with the JSON decoders for the typesfrontend/src/Core/Generated/ElmStreet.elm: Core.Generated.ElmStreet module with bundled helper functionsIf you want to use capabilities provided by elm-street in your Elm
application, you need to have several Elm packages preinstalled in the project. You
can install them with the following commands:
elm install elm/time
elm install elm/json
elm install NoRedInk/elm-json-decode-pipeline
elm install rtfeldman/elm-iso8601-date-strings
Elm-street is not trying to be as general as possible and support every
use-case. The library is opinionated in some decisions and contains several
limitations, specifically:
data UserStatus = UserStatus
{ userStatusId :: Id
, userStatusRemarks :: Text
}
data HealthReading = HealthReading
{ hrUser :: User
, hrDate :: UTCTime
, hrWeight :: Double
}
Elm instance which
will generate valid Elm defintions. Here is how you can create Elm instance for
newtypes with phantom type variables:
newtype Id a = Id { unId :: Text }
instance Elm (Id a) where
toElmDefinition _ = elmNewtype @Text "Id" "unId"
-- - Not supported
data Address
= Post { postCode :: Text }
| Full { fullStreet :: Text, fullHouse :: Int }
-- - Not supported
data Foo
= Bar Int Text
| Baz Int Int Text Text Double Double Bool Bool Char
elm-street generates type alias for
record data type. So the generated Elm type for the following Haskell data
type won't compile in Elm:
data User = User
{ userName :: Text
, userFollowers :: [User]
}
ToJSON/FromJSON instances from the aeson library except you need
to strip record field prefixes. Fortunately, this also can be done
generically. You can use functions from Elm.Aeson module to derive JSON
instances from the aeson package.UTCTime Haskell data type is supported and it's translated to Posix
type in Elm.ifthenelsecaseofletintypemodulewhereimportexposingasporttag (reserved for constructor name due to aeson options)FromJSON and ToJSON instances should be derived using newtype strategy. And Elm should be derived using anyclass strategy:
newtype Newtype = Newtype Int
deriving newtype (FromJSON, ToJSON)
deriving anyclass (Elm)
The frontend directory contains example of minimal Elm project that shows how
generated types are used. To play with this project, do:
generate-elm binary:
cabal new-run generate-elm
cabal new-run run-backend
frontend folder:
cd frontend
elm-app start
Below you can see some examples of how Haskell data types are converted to Elm
types with JSON encoders and decoders using the elm-street library.
Haskell
data User = User
{ userName :: Text
, userAge :: Int
} deriving (Generic)
deriving (Elm, ToJSON, FromJSON) via ElmStreet User
Elm
type alias User =
{ name : String
, age : Int
}
encodeUser : User -> Value
encodeUser x = E.object
[ ("name", E.string x.name)
, ("age", E.int x.age)
]
decodeUser : Decoder User
decodeUser = D.succeed User
|> required "name" D.string
|> required "age" D.int
Haskell
data RequestStatus
= Approved
| Rejected
| Reviewing
deriving (Generic)
deriving (Elm, ToJSON, FromJSON) via ElmStreet RequestStatus
Elm
type RequestStatus
= Approved
| Rejected
| Reviewing
showRequestStatus : RequestStatus -> String
showRequestStatus x = case x of
Approved -> "Approved"
Rejected -> "Rejected"
Reviewing -> "Reviewing"
readRequestStatus : String -> Maybe RequestStatus
readRequestStatus x = case x of
"Approved" -> Just Approved
"Rejected" -> Just Rejected
"Reviewing" -> Just Reviewing
_ -> Nothing
universeRequestStatus : List RequestStatus
universeRequestStatus = [Approved, Rejected, Reviewing]
encodeRequestStatus : RequestStatus -> Value
encodeRequestStatus = E.string << showRequestStatus
decodeRequestStatus : Decoder RequestStatus
decodeRequestStatus = elmStreetDecodeEnum readRequestStatus
Haskell
newtype Age = Age
{ unAge :: Int
} deriving (Generic)
deriving newtype (FromJSON, ToJSON)
deriving anyclass (Elm)
Elm
type alias Age =
{ age : Int
}
encodeAge : Age -> Value
encodeAge x = E.int x.age
decodeAge : Decoder Age
decodeAge = D.map Age D.int
Haskell
newtype Id a = Id
{ unId :: Text
} deriving (Generic)
deriving newtype (FromJSON, ToJSON)
instance Elm (Id a) where
toElmDefinition _ = elmNewtype @Text "Id" "unId"
Elm
type alias Id =
{ unId : String
}
encodeId : Id -> Value
encodeId x = E.string x.unId
decodeId : Decoder Id
decodeId = D.map Id D.string
Haskell
data Guest
= Regular Text Int
| Visitor Text
| Blocked
deriving (Generic)
deriving (Elm, ToJSON, FromJSON) via ElmStreet Guest
Elm
type Guest
= Regular String Int
| Visitor String
| Blocked
encodeGuest : Guest -> Value
encodeGuest x = E.object <| case x of
Regular x1 x2 -> [("tag", E.string "Regular"), ("contents", E.list identity [E.string x1, E.int x2])]
Visitor x1 -> [("tag", E.string "Visitor"), ("contents", E.string x1)]
Blocked -> [("tag", E.string "Blocked"), ("contents", E.list identity [])]
decodeGuest : Decoder Guest
decodeGuest =
let decide : String -> Decoder Guest
decide x = case x of
"Regular" -> D.field "contents" <| D.map2 Regular (D.index 0 D.string) (D.index 1 D.int)
"Visitor" -> D.field "contents" <| D.map Visitor D.string
"Blocked" -> D.succeed Blocked
c -> D.fail <| "Guest doesn't have such constructor: " ++ c
in D.andThen decide (D.field "tag" D.string)
Haskell
74.6%
Elm
20.3%
JavaScript
4.2%