DEV Community

Kir Axanov
Kir Axanov

Posted on

Code. Gleam. Extract fields from JSON

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 to then() is Ok, then we continue with it; if the result is Error - we stop there and return this error from the pipe. So it's important to have consistent Error types through the whole pipe.

So, here are the steps.

# Erlang version <= OTP26
gleam add gleam_json@1

# Erlang version >= OTP27
gleam add gleam_json@2
Enter fullscreen mode Exit fullscreen mode
  • 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
}
Enter fullscreen mode Exit fullscreen mode
  • 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)
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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\"}}}"))
}
Enter fullscreen mode Exit fullscreen mode

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)