Elm Json Decoder Pipeline error - json

I'm trying to decode some json coming in from an http request but I keep running into syntax issues. This is the error I get from the compiler:
-- TYPE MISMATCH ------------------------------------------------------ [7/1811$
The 2nd argument to function `send` is causing a mismatch.
65| Http.send CardFetch (Http.get url modelDecoder)
^^^^^^^^^^^^^^^^^^^^^^^^^
Function `send` is expecting the 2nd argument to be:
Http.Request String
But it is:
Http.Request Model
Here is my code:
module Main exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Http
import Json.Decode as Decode exposing (string, Decoder, at, index)
import Json.Decode.Pipeline exposing (..)
main : Program Never Model Msg
main =
program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
type alias Model =
{ boardName : String
, cardName : String
}
init =
( Model "Default Board" "Default Card"
, Cmd.none
)
-- UPDATE
type Msg
= CardFetch (Result Http.Error String)
| DataFetch
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
DataFetch ->
( model, getData )
CardFetch (Ok incomingName) ->
( Model model.cardName incomingName, Cmd.none )
CardFetch (Err errorMessage) ->
( model, Debug.log "Errors" Cmd.none )
-- HTTP
url : String
url =
"https://api.trello.com/1/members/user/actions?limit=3&key=..."
getData =
Http.send CardFetch (Http.get url modelDecoder)
{--decodeCard =
Decode.index 0
modelDecoder
(Decode.at
[ "data", "card", "name" ]
string
)
--}
modelDecoder : Decoder Model
modelDecoder =
decode Model
|> custom (index 0 (at [ "data", "card", "name" ] string))
|> custom (index 0 (at [ "data", "board", "name" ] string))
--UPDATE
-- VIEW
view : Model -> Html Msg
view model =
div []
[ div []
[ button [ onClick DataFetch ] [ text "Get Card" ] ]
, div [ class "card" ]
[ h3 [] [ text model.boardName ]
, div [ class "board" ] [ h4 [] [ text model.cardName ] ]
]
]
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
I'm am fairly new to Elm and I'm trying to figure out how API calls work. Elm documentation is excellent but the bit about API calls is kind of vague. I would appreciate any help I can get. Thanks a lot!

You declared in your messages:
CardFetch (Result Http.Error String)
which means that successful response will produce String. However, your modelDecoder is returning Model: modelDecoder : Decoder Model.
Changing your message declaration to:
CardFetch (Result Http.Error Model)
and in update function:
CardFetch (Ok incomingName) ->
( incomingName, Cmd.none )
should help.

Related

Elm: decoding json from http response and showing it

I'm kind of new to Elm and I find it very hard to decode a json from a http response.
The app I'm making is doing a call to gravatar and receives a profile.
I'd like to extract some fields from the response and put in in a record, which in turn in shown in the view.
This is my code:
-- MODEL
type alias MentorRecord =
{ displayName : String
, aboutMe : String
, currentLocation : String
, thumbnailUrl : String
}
type alias Model =
{ newMentorEmail : String
, newMentor : MentorRecord
, mentors : List MentorRecord
}
init : ( Model, Cmd Msg )
init =
( Model "" (MentorRecord "" "" "" "") [], Cmd.none )
-- UPDATE
type Msg
= MentorEmail String
| AddMentor
| GravatarMentor (Result Http.Error MentorRecord)
| RemoveMentor
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
MentorEmail newEmail ->
( { model | newMentorEmail = newEmail }, Cmd.none )
AddMentor ->
( model, getGravatarMentor model.newMentorEmail )
GravatarMentor (Ok addedMentor) ->
( Model "" addedMentor (addedMentor :: model.mentors)
, Cmd.none
)
GravatarMentor (Err _) ->
( model, Cmd.none )
RemoveMentor ->
( model, Cmd.none )
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Email adress mentor", onInput MentorEmail ] []
, button [ onClick AddMentor ] [ text "Add Mentor" ]
, br [] []
, img [ src (createIconUrl model.newMentorEmail) ] []
, div [] [ text model.newMentor.displayName ]
, div [] [ toHtmlImgList model.mentors ]
]
toHtmlImgList : List MentorRecord -> Html Msg
toHtmlImgList mentors =
ul [] (List.map toLiImg mentors)
toLiImg : MentorRecord -> Html Msg
toLiImg mentor =
li [] [ img [ src mentor.thumbnailUrl ] [] ]
-- HTTP
getGravatarMentor : String -> Cmd Msg
getGravatarMentor newMentorEmail =
Http.send GravatarMentor
(Http.get (createProfileUrl newMentorEmail) decodeGravatarResponse)
createProfileUrl : String -> String
createProfileUrl email =
"https://en.gravatar.com/" ++ MD5.hex email ++ ".json"
createIconUrl : String -> String
createIconUrl email =
"https://www.gravatar.com/avatar/" ++ MD5.hex email
decodeGravatarResponse : Decoder MentorRecord
decodeGravatarResponse =
let
mentorDecoder =
Json.Decode.Pipeline.decode MentorRecord
|> Json.Decode.Pipeline.required "displayName" string
|> Json.Decode.Pipeline.required "aboutMe" string
|> Json.Decode.Pipeline.required "currentLocation" string
|> Json.Decode.Pipeline.required "thumbnailUrl" string
in
at [ "entry" ] mentorDecoder
If a valid email address if filled in (i.e. one with a gravatar profile), you see the icon. But what this code also should do is extract name, location, about me info, thumbnailUrl from another http response, put it in a list, and show it in the view. And that's not happening if you click on 'Add mentor'
So I guess the decoding part isn't going very well, but I'm not sure (maybe because the nested element is in a list?).
A response from gravatar looks like this (removed some fields in entry):
{ "entry": [
{
"preferredUsername": "bla",
"thumbnailUrl": "https://secure.gravatar.com/avatar/hashinghere",
"displayName": "anne",
"aboutMe": "Something...",
"currentLocation": "Somewhere",
}
]}
Code in Ellie app: https://ellie-app.com/n5dxHhvQPa1/1
entry is an array. To decode the contents of the first element of the array, you need to use Json.Decode.index.
Change:
(at [ "entry" ]) mentorDecoder
to
(at [ "entry" ] << index 0) mentorDecoder
But the bigger problem here is that Gravatar does not support cross origin requests (CORS) but only JSONP. elm-http doesn't support JSONP. You can either use ports for that or use a third party service which enables you to make CORS requests to arbitrary sites. I've used the latter in the ellie link below but you should use ports or your own CORS proxy in a real production application.
I also made aboutMe and currentLocation optional as they weren't present in the profile I checked. Here's the link: https://ellie-app.com/pS2WKpJrFa1/0
The original functions:
createProfileUrl : String -> String
createProfileUrl email =
"https://en.gravatar.com/" ++ MD5.hex email ++ ".json"
decodeGravatarResponse : Decoder MentorRecord
decodeGravatarResponse =
let
mentorDecoder =
Json.Decode.Pipeline.decode MentorRecord
|> Json.Decode.Pipeline.required "displayName" string
|> Json.Decode.Pipeline.required "aboutMe" string
|> Json.Decode.Pipeline.required "currentLocation" string
|> Json.Decode.Pipeline.required "thumbnailUrl" string
in
at [ "entry" ] mentorDecoder
The changed functions:
createProfileUrl : String -> String
createProfileUrl email =
"https://crossorigin.me/https://en.gravatar.com/" ++ MD5.hex email ++ ".json"
decodeGravatarResponse : Decoder MentorRecord
decodeGravatarResponse =
let
mentorDecoder =
Json.Decode.Pipeline.decode MentorRecord
|> Json.Decode.Pipeline.required "displayName" string
|> Json.Decode.Pipeline.optional "aboutMe" string ""
|> Json.Decode.Pipeline.optional "currentLocation" string ""
|> Json.Decode.Pipeline.required "thumbnailUrl" string
in
(at [ "entry" ] << index 0) mentorDecoder

Conditional JSON decoding based on a field value

I have a need to decode JSON into an elm type like below:
Type
type User = Anonymous | LoggedIn String
type alias Model =
{ email_id : User
, id : Id
, status : Int
, message : String
, accessToken : AccessToken
}
JSON Message 1
{
"status": 0,
"message": "Error message explaining what happened in server"
}
into type value
Model {
"email_id": Anonymous
, id: 0
, status: 0
, message: json.message
, accessToken: ""
}
JSON Message 2
{
"status": 1,
"email_id": "asdfa#asdfa.com"
"token": "asdfaz.adfasggwegwegwe.g4514514ferf"
"id": 234
}
into type value
Model {
"email_id": LoggedIn json.email_id
, id: json.id
, status: json.status
, message: ""
, accessToken: json.token
}
Decoder information
Above, "message" is not always present and email_id/id/token are always not present.
How to do this type of conditional decoding in elm
Json.Decode.andThen lets you do conditional parsing based on the value of a field. In this case, it looks like you'll first want to pull out the value of the "status" field, andThen handle it separately based on whether it is a 1 or 0.
Edit 2016-12-15: Updated to elm-0.18
import Html as H
import Json.Decode exposing (..)
type User = Anonymous | LoggedIn String
type alias Id = Int
type alias AccessToken = String
type alias Model =
{ email_id : User
, id : Id
, status : Int
, message : String
, accessToken : AccessToken
}
modelDecoder : Decoder Model
modelDecoder =
(field "status" int) |> andThen modelDecoderByStatus
modelDecoderByStatus : Int -> Decoder Model
modelDecoderByStatus status =
case status of
0 ->
map5
Model
(succeed Anonymous)
(succeed 0)
(succeed status)
(field "message" string)
(succeed "")
1 ->
map5
Model
(map LoggedIn (field "email_id" string))
(field "id" int)
(succeed status)
(succeed "")
(field "token" string)
_ ->
fail <| "Unknown status: " ++ (toString status)
main = H.div []
[ H.div [] [ decodeString modelDecoder msg1 |> Result.toMaybe |> Maybe.withDefault emptyModel |> toString |> H.text ]
, H.div [] [ decodeString modelDecoder msg2 |> Result.toMaybe |> Maybe.withDefault emptyModel |> toString |> H.text ]
]
emptyModel = Model Anonymous 0 0 "" ""
msg1 = """
{
"status": 0,
"message": "Error message explaining what happened in server"
}
"""
msg2 = """
{
"status": 1,
"email_id": "asdfa#asdfa.com"
"token": "asdfaz.adfasggwegwegwe.g4514514ferf"
"id": 234
}
"""

How to remove List() and escape characters from response JSON in Scala

I have the following function that takes in a JSON input and validates it against a JSON-Schema using the "com.eclipsesource" %% "play-json-schema-validator" % "0.6.2" library. Everything works fine expect for when I get an invalid JSON, I try to collect all violations into a List and later return that list along with the response JSON. However my List is encoded with List() and also has escape characters. I want to have the response JSON look like this:
{
"transactionID": "123",
"status": "error",
"description": "Invalid Request Received",
"violations": ["Wrong type. Expected integer, was string.", "Property action missing"]
}
Instead of this: (This is what I am getting right now)
{
"transactionID": "\"123\"",
"status": "error",
"description": "Invalid Request Received",
"violations": "List(\"Wrong type. Expected integer, was string.\", \"Property action missing\")"
}
And here's the actual function for JSON validation
def validateRequest(json: JsValue): Result = {
{
val logger = LoggerFactory.getLogger("superman")
val jsonSchema = Source.fromFile(play.api.Play.getFile("conf/schema.json")).getLines.mkString
val transactionID = (json \ "transactionID").get
val result: VA[JsValue] = SchemaValidator.validate(Json.fromJson[SchemaType](
Json.parse(jsonSchema.stripMargin)).get, json)
result.fold(
invalid = { errors =>
var violatesList = List[String]()
var invalidError = Map("transactionID" -> transactionID.toString(), "status" -> "error", "description" -> "Invalid Request Received")
for (msg <- (errors.toJson \\ "msgs"))
violatesList = (msg(0).get).toString() :: violatesList
invalidError += ("violations" -> (violatesList.toString()))
//TODO: Make this parsable JSON list
val errorResponse = Json.toJson(invalidError)
logger.error("""Message="Invalid Request Received" for transactionID=""" + transactionID.toString() + "errorResponse:" + errorResponse)
BadRequest(errorResponse)
},
valid = {
post =>
db.writeDocument(json)
val successResponse = Json.obj("transactionID" -> transactionID.toString, "status" -> "OK", "message" -> ("Valid Request Received"))
logger.info("""Message="Valid Request Received" for transactionID=""" + transactionID.toString() + "jsonResponse:" + successResponse)
Ok(successResponse)
}
)
}
}
UPDATE 1
I get this after using Json.obj()
{
"transactionID": "\"123\"",
"status": "error",
"description": "Invalid Request Received",
"violations": [
"\"Wrong type. Expected integer, was string.\"",
"\"Property action missing\""
]
}
I got the escape characters removed by modifying this line:
violatesList = (msg(0).get).toString() :: violatesList
TO:
violatesList = (msg(0).get).as[String] :: violatesList
What you want is a JSON array, but by calling .toString() on your list, you're actually passing a string. Play has an implicit serializer for Lists to JSON arrays, so you actually just have to do less then what you already did - you can just remove the toString() part from violatesList.toString().
In addition, don't create a map for your JSON and then convert it to a JSON, you can use Json.obj with a very similar syntax instead:
val invalidError = Json.obj("transactionID" -> transactionID, "status" -> "error", "description" -> "Invalid Request Received")
for (msg <- (errors.toJson \\ "msgs"))
violatesList = (msg(0).get) :: violatesList
val errorResponse = invalidError ++ Json.obj("violations" -> violatesList)
Regarding your escaped quotes, I suppose it's because transactionID and msgs are JsStrings, so when you convert them with toString() the quotes are included. Just remove the toString everywhere and you'll be fine.

Unable to parse Json as a Map - Scala/Play

I'm unable to parse the json I'm getting as a Map. Anyone have any ideas? Please do ask if you require any more information. Thanks :)
Trying to parse the following response using:
Json.parse(response.body).as[Map[String, Either[List[ErrorMsg], Seq[OepPoint]]]]
Response:
{
"Payout": {
"errors":[
{
"field": "Last point: OepPoint(0.033,72.14). Current: OepPoint(0.033,65.71)",
"message":"OEP must be unique"
}
],
"curve":[]
}
}
Error Message thrown is:
No Json deserializer found for type Map[String,Either[List[ErrorMsg],Seq[OepPoint]]]. Try to implement an implicit Reads or Format for this type.
[error] val errorExpected = Json.parse(response.body).as[Map[String, Either[List[ErrorMsg], Seq[OepPoint]]]]
[error] ^
[error] one error found
Structure of OepPoint:
case class OepPoint(oep: Double, loss: Double)
object OepPoint {
implicit val oepPointReads = Json.format[OepPoint]
}
Structure of ErrorMsg:
case class ErrorMsg(field: String, message: String)
object ErrorMsg {
implicit val errorMsgReads = Json.format[ErrorMsg]
}
Assuming you have proper Reads for OepPoint and ErrorMsg. You can do following.
case class ErrOrOep( errors: List[ ErrorMsg ], curve: List[ OepPoint ] )
implcit val errOrOepFormat = Json.format[ ErrOrOep ]
val jsonMap = Json.parse(response.body).as[ Map[String, ErrOrOep ] ]
val errOrOep = jsonMap( "Payout" )
val oepEither: Either[ List[ ErrorMsg ], List[ OepPoint ] ] =
( errOrOep.errors, errOrOep.curve ) match {
case ( _, h :: _ ) => Right( errOrOep.curve )
case ( h :: _, _ ) => Left( errOrOep.error )
case ( _, _ ) => Left( errOrOep.error )
}

Play Framework: How to replace all the occurrence of a value in a JSON tree

Given the following JSON...
{ "id":"1234",
"name" -> "joe",
"tokens: [{
"id":"1234",
"id":"2345"
}]
}
... I need to replace the value of all the ids by xxxx like this:
{ "id":"xxxx",
"name" -> "joe",
"tokens: [{
"id":"xxxx",
"id":"xxxx"
}]
}
Let's start create the JSON tree:
val json = Json.obj(
"id" -> "1234",
"name" -> "joe",
"tokens" -> Json.arr(
Json.obj("id" -> "1234"),
Json.obj("id" -> "2345")
)
)
json: play.api.libs.json.JsObject = {"id":"1234","name":"joe","tokens":[{"id":"1234"},{"id":"2345"}]}
Then, getting all the ids is very simple:
json \\ "id"
res64: Seq[play.api.libs.json.JsValue] = List("1234", "1234", "2345")
Now, how do I replace the value of all the ids by xxxx?
There doesn't appear to be a nice way to do this with the standard Play JSON library, although I'd be happy to be proved wrong in that regard. You can however do it easily using the play-json-zipper extensions:
import play.api.libs.json._
import play.api.libs.json.extensions._
val json = Json.obj(
"id" -> "1234",
"name" -> "joe",
"tokens" -> Json.arr(
Json.obj("id" -> "1234"),
Json.obj("id" -> "2345")
)
)
// Using `updateAll` we pattern match on a path (ignoring
// the existing value, as long as it's a string) and replace it
val transformed = json.updateAll {
case (__ \ "id", JsString(_)) => JsString("xxxx")
}
// play.api.libs.json.JsValue = {"id":"xxxx","name":"joe","tokens":[{"id":"xxxx"},{"id":"xxxx"}]}
To make that a re-usable function:
def replaceValue(json: JsValue, key: String, replacement: String) = json.updateAll {
case (__ \ path, JsString(_)) if path == key => JsString(replacement)
}
The json-zipper extensions are still "experimental", but if you want to add them to your project add the following to your project/Build.scala appDependencies:
"play-json-zipper" %% "play-json-zipper" % "1.0"
and the following resolver:
"Mandubian repository releases" at "https://github.com/mandubian/mandubian-mvn/raw/master/releases/"
Probably it isn't most efficient way to do it, but you can try to convert your JSON to an object copy it with new fields and then convert it back to json. Unfortunately currently I don't have environment to check the code, but it should be something like this:
case class MyId(id: String)
case class MyObject(id: String, name: String, tokens: List[MyId])
implicit val idFormat = Json.format[MyId]
implicit val objectFormat = Json.format[MyObject]
val json = Json.parse(jsonString)
val jsResult = Json.fromJson[MyObject](json)
val obj = jsResult match {
case JsSuccess(s, _) => s
case _ => throw new IllegalStateException("Unexpected")
}
val newObj = obj.copy(id = "xxxx")
val result = Json.toJson(newObj)