#2874 JSON Usage

Nick Sun 21 Aug 2022

Hello all.

Brand new Fantom user so forgive me if the question is elementary. Excited to learn but currently going through the early stage bumps of learning a new language. My background is mostly in Julia, some in Python, and very basic Java for context.

I'm struggling to understand the proper usage of the JsonInStream functionality.

I have an API that is returning a hierarchical dataset something like this:

{"ResultSet": {
 "total": 1,
 "offset": 0,
 "limit": 100,
 "pages": 0,
 "size": 1,
 "metadata": null,
 "Results": [
  {
   "dto": "TableX",
   "dtoState": "UNMODIFIED",
   "fields": {
   	    "assetTag": "10001",
    "description": "0001-HVAC-AHU-2B",
    "statusCode": "ACTIVE"
    }
   }
  ]
 }
}

(Pardon the brace misplacement, MD was fighting me for some reason)

I am using the following where c is a WebClient being used for the request

data := JsonInStream(c.resIn).readJson

To do what I think is assign the Fantom object representation to a variable and this works but when I try to index into this object by

data["ResultSet"]

I get

ERROR(9): No operator method found: sys::Obj? [ sys::Str ]

I've used

Str:Obj? data := JsonInStream(c.resIn).readJson

and this will let me index into the first level via

data["ResultSet"]

but after that I run into the same issue.

What am I missing in the usage here that is leaving me unable to access any lower level data in this response?

brian Sun 21 Aug 2022

You are on the right track, but just need a few more casts.

The readJson method returns an Obj? because it could be a JSON object (which is a Fantom Str:Obj), or it could be a JSON array (Fantom list), or it could just a be a scalar value like Str, Float, or Bool. So its up to you to cast each level of the JSON tree based on your data.

Probably what you want is something like this:

// top level is a map
top := (Str:Obj?)JsonInStream(c.resIn).readJson

// result set is also a map
resultSet := (Str:Obj?)top["ResultSet"]

// total is a number
total := (Int)resultSet["total"]

// results is a list
results := (Obj?[])resultsSet("Results")

Nick Sun 21 Aug 2022

Copy that. Thank you for the answer.

I had a sneaking suspicion that might be the case but it seems very unintuitive.

If I may ask, I'm curious what drove this design choice. It seems like the JSON format is self describing and thus any JSON string could be represented as its own object with object representations of all needed components through it's full depth.

Using the example of the results I'm getting from the API, the fact that the JSON string represents the values of total, offset, limit, pages, and size without quotes shows they should be numbers and the inclusion of quotes on the value field of assetTag shows it will be a string. Additionally the format of {K:V} should indicate a map and [things] indicates a list.

Again using my example above we can tell from the JSON we have a hierarchy like Map

-Map

-List
  -Map
     -Map

I know that once I have the results, but I'm not clear on why it is left to the user to specify when it can be inferred from the JSON format itself and the types of each element can also be inferred from the format. If I am expecting a return value whose structure is unknown, I have to get the value before I can create the object.

Please don't take this as negativity or a complaint, genuine curiosity here. So far I've found the language to be very well thought out, so I'm confident there is a good reason for the design choice, I'm just trying to better understand that and how it drives the way I can effectively use it. I'm not sure how to proceed with object construction for responses of unknown structure based on the current setup.

SlimerDude Sun 21 Aug 2022

Hi Nick, it's not really a design choice, but the difference between what is known at Compile Time vs what is known at Runtime.

The information you are referring to (is the Obj a Map, List, Str, etc...) is only known at Runtime - because the JSON object being parsed could be anything.

Indeed, at Runtime the objects are a Map, is a List, etc. But for code to compile correctly, casts are needed to help the compiler known what objects you are expecting.

Nick Sun 21 Aug 2022

I'm still not understanding. I don't see how it could be a runtime vs compile time issue. The rules for construction of a JSON string are known at compile time.

Even though a JSON object could be anything we know that no matter what it is it will follow the rules of being a JSON object.

Just like a string could be any combination of letters or characters, we don't need to know at compile time what those characters are to be able to take them and base 64 encode or decode them. We just need to have agreed ahead of time on what the encoding scheme is.

Henry Sun 21 Aug 2022

Hi Nick, I can't comment on anything regarding Runtime vs Compile, but I can offer an alternate solution.

If you're aware of the format you're expecting the Json to be returned in, I would recommend giving the afJson library a try. It will allow you to set up Fantom Classes to represent your Json objects and convert to and from the expected types, which allows you to convert from Json without the need to repeatedly cast to the expected classes. User Guide

It's more effort development wise to set up all the classes initially, but in my opinion it's the cleanest way to work with Json in Fantom if you're going to be doing a lot of it (Plus it makes for much cleaner code allowing you to avoid long chains of casts or traps!)

Login or Signup to reply.