How to access a JSON structure in a functional manner

In ujson I have JSON structures like this:

{ "input": "alfred",
  "result": [
    {
      "name": "Alfred Pulipuli",
      "job": "hair dresser",
      "location": {
        "x": 149.1780959,
        "y": -35.24640164
      },
      "more": {
        "member": true,
        "certified": "level1"
      }
    },
    {
      "name": "Alfred J Carubel",
      "job": "bear trimmer",
      "more": {
        "member": false,
        "certified": "level1"
      }
    }]
}

From these structures I want extract the job descriptions (here [ "hair dresser", "bear trimmer"]), which is easy to do like data("result")....

But what if the input JSON in data does not adhere to this structure?

Well, I can use exception handling:

Try:
    data("result")...
match:
    case Success(result) => result
    case Failure(_) => ujson.Arr()

However I think this is not v ery functional, because I use exception handling. What would be a more functional approach? Defining first a schema? (I don’t care whether the other JSON attributes are correct.)

All pointers are appreciated!

Well, “functional” is kind of an overloaded word. IMHO, if what you have works, gives you enough detail, and you don’t need to change it much, then that is good enough.

But, if you prefer, then yeah, you could avoid a single Try block and rather try to compose individual bits. I wonder if uJson provides you with “safe” access operations that would give a Try or an Either instead of throwing, but probably no, in that case, just wrap each specific access.
Then, you could compose each individual error using combinators and for. I have done that using other libraries like circe, the pseudocode would look like this:

def extracttJobs(data: Json): EitherNec[Error, List[Job]] =
  for
    rawResults <- data.get(key = "results").toEither(left = Error("Data is missing the 'results' field"))
    results <- rawReults.as[List[Json]]
    jobs <- results.parTraverse { result =>
      result
        .get(key = "job")
        .toEither(left = Error("A result is missing the 'job' field"))
        .flatMap(_.as[Job])
    }
  yield jobs 

Another option would be indeed to simply model the data and let uJson decode directly to a custom case class. It should handle all failures there.

3 Likes

Thanks for this, @BalmungSan.

I’ll look at circe – it might be more functional in its core than uJson.

Ok, since you asked for “any pointers”..

Reiterating what @BalmungSan noted, I’d urge you to graduate from thinking of things as “more functional” (== more good) to a more precise mental vocabulary. I say this as someone who’s a fan of functional programming and does it more or less 100% of the time.

“more functional” is a very imprecise, overused, overloaded concept; try to replace it with cleaner, clearer, more specific ideas as soon as possible. It reminds me of how I played soccer in primary school: “always try to kick the ball in one direction”; sort of works, but try to level up to the next tier quickly.

In this case, the concept is error handling without exceptions, eg by using a type like Either[Error, A]. Honestly, though, when I see this advocated people often don’t acknowledge just how much machinery is needed to apply error handling without exceptions successfully.

The core objection to exceptions is “why do we need a special execution mode & syntax for Exceptions when we can build it out of functions, values and monadic composition using regular execution mode, regular syntax, regular types and short-circuiting monads?”.

But it does require comfort with monadic programming, which itself requires learning special syntaxes (I’d argue even deep-nested flatMaps is a special syntax IMO).

Then the question arises of how to combine error values with other types of effects, like asynchrony. A key ideological fork between Cats Effect and ZIO.

In your specific example, all paths are variants of the same choice. Either the JSON has the required structure, or it doesn’t. A schema is just another way of determining expected structure.

You can either return a 2-sided value, like you already do with the Try. The functional way. Or throw an exception and return a 1-sided happy-case structure.

A key question you need to answer is, when the JSON is invalid, what do you want to happen? You’ve returned an empty array. Does this simply push the problem downstream? If the answer is, the program cannot proceed on invalid input, then letting it throw may be a good choice for now.

1 Like

Thanks for your deep thoughts, @benhutchison.

I totally agree, the case at hand is about what to do in the error case. – And functional programming constructs force me to think of it early.