Hi!
Sometimes we need to get just a couple of fields from a JSON string without any complex stuff like writing decoders or using a JSON-schema.
We will use
result.then()
for easy piping: if the result, passed tothen()
isOk
, then we continue with it; if the result isError
- we stop there and return this error from the pipe. So it's important to have consistentError
types through the whole pipe.
So, here are the steps.
- Install gleam_json package:
# Erlang version <= OTP26
gleam add gleam_json@1
# Erlang version >= OTP27
gleam add gleam_json@2
- Write some types in
types.gleam
:
pub type HttpError {
// ...
JsonParseError(error: JsonParseErrorType, field: String, json_string: String)
}
pub type JsonParseErrorType {
InvalidJsonForParsing
ObjectFieldNotFound
IntegerFieldNotFound
FloatFieldNotFound
StringFieldNotFound
SeveralFieldsNotFound
}
- Write some very basic utils to shorten the
gleam_json
usage syntax and simplify piping process. Here I've added only some funs for parsing root JSON as an object (it can also be an array) and extracting integers, floats and strings (it won't be hard to add other ones by yourself):
import gleam/string
import gleam/result.{then, replace_error}
import gleam/json
import gleam/dynamic
import gleam/dict
import types.{
type HttpError, JsonParseError,
InvalidJsonForParsing,
ObjectFieldNotFound,
IntegerFieldNotFound,
FloatFieldNotFound,
StringFieldNotFound,
}
/// Parses JSON string as an object into a dictionary.
pub fn parse_obj(json_string: String) -> Result(dict.Dict(String, dynamic.Dynamic), HttpError) {
json_string
|> json.decode(dynamic.dict(dynamic.string, dynamic.dynamic))
|> replace_error(JsonParseError(error: InvalidJsonForParsing, field: "", json_string: json_string))
}
/// Retrieves an object field from the current JSON level.
pub fn get_obj(
body: dict.Dict(String, dynamic.Dynamic),
field: String,
) -> Result(dict.Dict(String, dynamic.Dynamic), HttpError) {
body
|> dict.get(field)
|> then(as_dict())
|> replace_error(JsonParseError(error: ObjectFieldNotFound, field: field, json_string: string.inspect(body)))
}
/// Retrieves an integer field from the current JSON level.
pub fn get_int(
body: dict.Dict(String, dynamic.Dynamic),
field: String,
) -> Result(Int, HttpError) {
body
|> dict.get(field)
|> then(as_int())
|> replace_error(JsonParseError(error: IntegerFieldNotFound, field: field, json_string: string.inspect(body)))
}
/// Retrieves a float field from the current JSON level.
pub fn get_float(
body: dict.Dict(String, dynamic.Dynamic),
field: String,
) -> Result(Float, HttpError) {
body
|> dict.get(field)
|> then(as_float())
|> replace_error(JsonParseError(error: FloatFieldNotFound, field: field, json_string: string.inspect(body)))
}
/// Retrieves a string field from the current JSON level.
pub fn get_string(
body: dict.Dict(String, dynamic.Dynamic),
field: String,
) -> Result(String, HttpError) {
body
|> dict.get(field)
|> then(as_string())
|> replace_error(JsonParseError(error: StringFieldNotFound, field: field, json_string: string.inspect(body)))
}
/// Replacement for `dynamic.dict(dynamic.string, dynamic.dynamic)` to have a custom `Error` for piping.
fn as_dict() -> fn(dynamic.Dynamic) -> Result(dict.Dict(String, dynamic.Dynamic), Nil) {
fn(body: dynamic.Dynamic) {
body
|> dynamic.dict(dynamic.string, dynamic.dynamic)
|> replace_error(Nil)
}
}
/// Replacement for `dynamic.int(_)` to have a custom `Error` for piping.
fn as_int() -> fn(dynamic.Dynamic) -> Result(Int, Nil) {
fn(body: dynamic.Dynamic) {
body
|> dynamic.int()
|> replace_error(Nil)
}
}
/// Replacement for `dynamic.float(_)` to have a custom `Error` for piping.
fn as_float() -> fn(dynamic.Dynamic) -> Result(Float, Nil) {
fn(body: dynamic.Dynamic) {
body
|> dynamic.float()
|> replace_error(Nil)
}
}
/// Replacement for `dynamic.string(_)` to have a custom `Error` for piping.
fn as_string() -> fn(dynamic.Dynamic) -> Result(String, Nil) {
fn(body: dynamic.Dynamic) {
body
|> dynamic.string()
|> replace_error(Nil)
}
}
- Use your new utils:
pub fn main() {
// {
// "name": "Lucy",
// "stats": {
// "class": "Barbarian",
// "power": 6,
// "max_hp": 10
// },
// "pets": {
// "Wolfie": {
// "type": "dog"
// }
// }
// }
let json_string = "{\"name\": \"Lucy\",\"stats\": {\"class\": \"Barbarian\",\"power\": 6,\"max_hp\": 10},\"pets\": {\"Wolfie\": {\"type\": \"dog\"}}}"
let json_dict = json_string |> json.parse_obj()
// Get Lucy's name
json_dict
|> then(json.get_string(_, "name"))
|> io.debug()
// Ok("Lucy")
// Get Wolfie's type
json_dict
|> then(json.get_obj(_, "pets"))
|> then(json.get_obj(_, "Wolfie"))
|> then(json.get_string(_, "type"))
|> io.debug()
// Ok("dog")
// Get something ridiculous
// Note that we get an error on extracting the `nonsense` field and don't go to `type` out of the box
json_dict
|> then(json.get_obj(_, "stats"))
|> then(json.get_obj(_, "nonsense"))
|> then(json.get_string(_, "type"))
|> io.debug()
// Error(JsonParseError(ObjectFieldNotFound, "nonsense", "dict.from_list([#(\"class\", \"Barbarian\"), #(\"max_hp\", 10), #(\"power\", 6)])"))
// Get something even more ridiculous
// Now we handle both fields and raising a `SeveralFieldsNotFound` error
let nonesense =
json_dict
|> then(json.get_obj(_, "stats"))
|> then(json.get_string(_, "nonesense"))
let delirious =
json_dict
|> then(json.get_obj(_, "stats"))
|> then(json.get_int(_, "delirious"))
case nonesense, delirious {
Ok(nonesense), Ok(delirious) -> Ok(#(nonesense, delirious))
Error(_), Error(_) -> Error(JsonParseError(error: SeveralFieldsNotFound, field: "stats.{nonesense, delirious}", json_string: json_string))
Error(err), _ -> Error(err)
_, Error(err) -> Error(err)
}
|> io.debug()
// Error(JsonParseError(SeveralFieldsNotFound, "stats.{nonesense, delirious}", "{\"name\": \"Lucy\",\"stats\": {\"class\": \"Barbarian\",\"power\": 6,\"max_hp\": 10},\"pets\": {\"Wolfie\": {\"type\": \"dog\"}}}"))
}
Bye!
P.S.
Sorry for poor syntax highlighting. Gleam isn't supported and I've chosen Erlang. That's better than a plain text, I guess 🤷♂️
Top comments (0)