How to map parsed JSON as JSON::Any type to custom Object?
In my case, I am working on chat client. Chat API can respond to requests with following JSON:
{"ok" => true,
"result" =>
[{"update_id" => 71058322,
"message" =>
{"message_id" => 20,
"from" => "Benjamin",
"text" => "hey"}}]}
Somewhere inside my API client code I parse these JSON to perform some basic health checks and pass the result to response consumer. In the consumer, I iterate over result array and try to convert each update to appropriate object:
module Types
class Update
JSON.mapping({
update_id: {type: Int32},
message: {type: Message},
})
end
end
module Types
class Message
JSON.mapping({
message_id: Int32,
date: Int32,
text: String,
})
end
end
return unless response["ok"]
response["result"].each do |data|
update = Types::Update.from_json(data)
end
Unfortunately, last line results in compile error:
no overload matches 'JSON::Lexer.new' with type JSON::Any
Apparently, Object.from_json can accept only String JSONs, but not parsed JSON. In my case data is JSON::Any object.
Dirty fix Types::Update.from_json(data.to_json) works, but it looks ridiculous.
What is the proper way to map JSON object to custom type preserving all nested structure?
JSON.mapping doesn't work nicely together with JSON.parse. To solve your problem you can create another mapping Types::Result and parse a hole json using Object.from_json which is even much more convenient to work with:
module Types
class Message
JSON.mapping(
message_id: Int32,
text: String
)
end
class Update
JSON.mapping(
update_id: Int32,
message: Message
)
end
class Result
JSON.mapping(
success: { key: "ok", type: Bool },
updates: { key: "result", type: Array(Update) }
)
end
end
result = Types::Result.from_json string_json
result.success # => true
result.updates.first.message.text # => "hey"
Related
What I would like to have happen: Someone can make a post request to users/new with parameters, and I would like to create a User object from the JSON parameters.
In the readme, it gives this example:
foo = Foo.from_json(%({"name": "Granite1"}))
But when I try to do this I get this compile-time error:
in /usr/local/Cellar/crystal/0.26.1/src/json/pull_parser.cr:13: no
overload matches 'JSON::Lexer.new' with type Hash(String, Array(JSON::Any) | Bool | Float64 | Hash(String, JSON::Any) | Int64 | String | Nil)
Overloads are:
- JSON::Lexer.new(string : String)
- JSON::Lexer.new(io : IO)
- JSON::Lexer.new()
#lexer = Lexer.new input
^~~
Here is what env.params.json looks like when logged to the console:
{"name" => "test",
"username" => "tester",
"email" => "test",
"password" => "test"}
Any help would be much appreciated.
The compiler is steering you in the right direction here. It looks like you're passing in a variable that, at compile-time, has the type Hash(String, V) where V is one of the types
Array(JSON::Any)
Bool
Float64
Hash(String, JSON::Any)
Int64
String
Nil
What it's expecting is a String (or an IO object, which is similar to a String) of JSON. That's what you have in the example. %(foo) is another way to create the String "foo" (See "Percent string literals" in the guide for more info). They're using it here because it allows you to avoid escaping the double quotes used in the JSON.
Based on the compile-time type that Crystal has given your parameter, my guess is that it's already been converted from JSON into a Crystal Hash. Double-check that you're not parsing it twice.
Without seeing the source, there's not much more information I can provide, but I hope that helps.
Let's say I have following class Message representing a message in a chat.
class Message
JSON.mapping({
message_id: {type: Int32},
text: {type: String, nilable: true},
photo: {type: Photo, nilable: true},
sticker: {type: Sticker, nilable: true},
})
MESSAGE_TYPES = %w( text photo sticker )
{% for type in MESSAGE_TYPES %}
def {{ type.id }}?
! {{ type.id }}.nil?
end
{% end %}
end
Every message can be either text, or photo, or sticker. But only one of those. For instance, if message is a text, then photo and sticker properties are nil, but text is not nil. If message is a sticker, only sticker is not nil. And so on. Real class has much more direct types and inferred types.
Upon receiving this message, I want to process it in a way specific for particular type. Something like:
case message
when .text?
p message.text
when .photo?
p message.photo.file_id
when .sticker?
p message.sticker.file_id
end
Of course, it won't work, because in each when clause corresponding properties are nilable and undefined method 'file_id' for Nil.
Is there any way to restrict variable type in each case making sure that is message.sticker? then message.sticker is not nil and file_id exists?
Maybe I'm completely wrong in very approaching the problem and there better and cleaner ways to code this?
require "json"
class Photo
JSON.mapping(url: String)
end
struct Message
JSON.mapping(id: Int32, message: String|Photo)
end
p Message.from_json(%({"id": 5, "message": {"url": "http://example.org/"}}))
You could put all payload objects in one instance variable with a union type as Oleh Prypin suggested.
Another option, if you want to keep the current data layout, you could have the methods return the respective Type or nil and assign its value to a variable:
if text = message.text?
p text
if photo = message.photo?
p photo.file_id
if sticker = message.sticker?
p sticker.file_id
end
When writing a unit test in phoenix framework how do you check if a json response contains a list.
Existing test is below which fails because children gets populated. I just want the test to tell me that my json response contains children and children is a list.
test "shows chosen resource", %{conn: conn} do
parent = Repo.insert! %Parent{}
conn = get conn, parent_path(conn, :show, parent)
assert json_response(conn, 200)["data"] == %{"id" => parent.id,
"children" => []}
end
I would use three asserts for this, using a pattern match assert first to assert the basic structure and extract id and children:
assert %{"id" => id, "children" => children} = json_response(conn, 200)["data"]
assert id == parent.id
assert is_list(children)
Note that this test will pass even if the map contains keys other than id and children.
With [json schema][2] you can generate a json to use with (https://github.com/jonasschmidt/ex_json_schema) to validate a full json structure.
iex> schema = %{
"type" => "object",
"properties" => %{
"foo" => %{
"type" => "string"
}
}
} |> ExJsonSchema.Schema.resolve
and
iex> ExJsonSchema.Validator.valid?(schema, %{"foo" => "bar"})
and remember have only one logical assertion per test” (http://blog.stevensanderson.com/2009/08/24/writing-great-unit-tests-best-and-worst-practises/)
I'm writing a play Controller that should call another web service and return its result verbatim -- same response code, same headers, same body. But it seems like the Controller is written such that I have to specify an explicit return code. I've tried getting the ahcResponse, but that doesn't seem to provide an obvious solution.
Here's what I have now:
def route(name: String, command: String) = Action {
Async {
(
WS.url("someurl").get().map {
(
response => Ok(response.body))
})
}
}
However, this always returns an "OK" status, and if it gets an error, it will pull the error HTML into the body as text.
How do I forward the results of a WS call back to my caller?
You could forward the response code and body in the following way:
WS.url(url)
.get
.map(response =>
response.status match {
// in case you want to do something special for ok
// otherwise, pattern matching is not necessary
case OK => Ok(response.body)
case x => new Status(x)(response.body)
})
.recover {
case ex: Throwable =>
InternalServerError("some exception...")
}
I'm getting an error on compile with the following code.
I'm trying to call a Web Service.
def authenticate(username: String, password: String): String = {
val request: Future[Response] =
WS.url(XXConstants.URL_GetTicket)
.withTimeout(5000)
.post( Map("username" -> Seq(username), "password" -> Seq(password) ) )
request map { response =>
Ok(response.xml.text)
} recover {
case t: TimeoutException =>
RequestTimeout(t.getMessage)
case e =>
ServiceUnavailable(e.getMessage)
}
}
I'm seeing the following compiler error:
type mismatch; found : scala.concurrent.Future[play.api.mvc.SimpleResult[String]] required: String
The value being returned from your authenticate function is val request = ... which is of type Future[Response] but the function expects a String which as the compiler says is a type mismatch error. Changing the return type of the function to Future[Response] or converting request to a String before returning it should fix it.
Like say Brian, you're currently returning a Future[String], when you method said that you want to return a String.
The request return a Future because it's an asynchronous call.
So, you have two alternatives:
Change your method definition to return a Future[String], and manage this future in another method (with .map())
Force the request to get this result immediately, in a synchronous way. It's not a very good deal, but sometimes it's the simplest solution.
import scala.concurrent.Await
import scala.concurrent.duration.Duration
val response: String = Await.result(req, Duration.Inf)