Reading JSON data with NSJSONSerialization

See Also: Writing JSON data with NSJSONSerialization

Here’s a snippet of code demonstrating how to read JSON data and convert it into an AppleScript record.

Here is a sample JSON file (test.json):

{"menu": {
  "id": "file",
  "value": "File",
  "popup": {
    "menuitem": [
      {"value": "New", "onclick": "CreateNewDoc()"},
      {"value": "Open", "onclick": "OpenDoc()"},
      {"value": "Close", "onclick": "CloseDoc()"}
    ]
  }
}}

And here is the AppleScript code to read it:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"

-- classes, constants, and enums used
property NSJSONSerialization : a reference to current application's NSJSONSerialization
property NSData : a reference to current application's NSData

set theFile to choose file

set theJSONData to NSData's dataWithContentsOfFile:(theFile's POSIX path)
set theJSON to NSJSONSerialization's JSONObjectWithData:theJSONData options:0 |error|:(missing value)

theJSON as record

--> {
--	|menu|:{
--		|id|:"file", 
--		value:"File", 
--		popup:{
--			menuitem:{
--				{
--					value:"New", 
--					onclick:"CreateNewDoc()"
--				}, 
--				{
--					value:"Open", 
--					onclick:"OpenDoc()"
--				}, 
--				{
--					value:"Close", 
--					onclick:"CloseDoc()"
--				}
--			}
--		}
--	}
--}

Read JSON Data.zip (241.9 KB)

5 Likes

A minor point to bear in mind is that the JSON spec allows for the top level of a JSON string to be either an Object or an Array, (thus mapping either to a record or to a list in AppleScript).

The following, for example, aims to allow for either case:

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

property pJSONArray : "[{\"menu\": {\n  \"id\": \"file\",\n  \"value\": \"File\",\n  \"popup\": {\n    \"menuitem\": [\n      {\"value\": \"New\", \"onclick\": \"CreateNewDoc()\"},\n      {\"value\": \"Open\", \"onclick\": \"OpenDoc()\"},\n      {\"value\": \"Close\", \"onclick\": \"CloseDoc()\"}\n    ]\n  }\n}}]"
property pJSONObject : "{\"menu\": {\n  \"id\": \"file\",\n  \"value\": \"File\",\n  \"popup\": {\n    \"menuitem\": [\n      {\"value\": \"New\", \"onclick\": \"CreateNewDoc()\"},\n      {\"value\": \"Open\", \"onclick\": \"OpenDoc()\"},\n      {\"value\": \"Close\", \"onclick\": \"CloseDoc()\"}\n    ]\n  }\n}}"


-- readJSON :: String -> a
on readJSON(strJSON)
    set ca to current application
    set {x, e} to ca's NSJSONSerialization's ¬
        JSONObjectWithData:((ca's NSString's stringWithString:strJSON)'s ¬
            dataUsingEncoding:(ca's NSUTF8StringEncoding)) ¬
            options:0 |error|:(reference)
    
    if x is missing value then
        error e's localizedDescription() as text
    else
        item 1 of ((ca's NSArray's arrayWithObject:x) as list)
    end if
end readJSON

on run
    {readJSON(pJSONArray), readJSON(pJSONObject)}
end run


That’s a good approach to conversion if it could be any of several classes, but if it’s a simple choice between two classes it adds a fair amount of unnecessary overhead. Probably better to do something specific like:

if x's isKindOfClass:(current application's NSDictionary) then return x as record
return x as list

In my tests that reduces the time taken for the whole of your script by about 10%.

1 Like

Very useful – thank you.

(Incidentally, I personally prefer to handle parse failures with an option type, rather by raising an error, so I would tend, in practice, to write something like this):

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

property pJSONArray : "[{\"menu\": {\n  \"id\": \"file\",\n  \"value\": \"File\",\n  \"popup\": {\n    \"menuitem\": [\n      {\"value\": \"New\", \"onclick\": \"CreateNewDoc()\"},\n      {\"value\": \"Open\", \"onclick\": \"OpenDoc()\"},\n      {\"value\": \"Close\", \"onclick\": \"CloseDoc()\"}\n    ]\n  }\n}}]"

property pJSONObject : "{\"menu\": {\n  \"id\": \"file\",\n  \"value\": \"File\",\n  \"popup\": {\n    \"menuitem\": [\n      {\"value\": \"New\", \"onclick\": \"CreateNewDoc()\"},\n      {\"value\": \"Open\", \"onclick\": \"OpenDoc()\"},\n      {\"value\": \"Close\", \"onclick\": \"CloseDoc()\"}\n    ]\n  }\n}}"

property pJSONillFormed : "{\"\"menu\": {\n  \"id\": \"file\",\n  \"value\": \"File\",\n  \"popup\": {\n    \"menuitem\": [\n      {\"value\": \"New\", \"onclick\": \"CreateNewDoc()\"},\n      {\"value\": \"Open\", \"onclick\": \"OpenDoc()\"},\n      {\"value\": \"Close\", \"onclick\": \"CloseDoc()\"}\n    ]\n  }\n}}"


-- readJSONLR :: String -> Either String a
on readJSONLR(strJSON)
    set ca to current application
    set {x, e} to ca's NSJSONSerialization's ¬
        JSONObjectWithData:((ca's NSString's stringWithString:strJSON)'s ¬
            dataUsingEncoding:(ca's NSUTF8StringEncoding)) ¬
            options:0 |error|:(reference)
    
    if x is missing value then
        |Left|(e's localizedDescription() as text)
    else
        if x's isKindOfClass:(ca's NSDictionary) then
            |Right|(x as record)
        else
            |Right|(x as list)
        end if
    end if
end readJSONLR

-- TEST ------------------------------------------------------------------
on run
    {readJSONLR(pJSONArray), ¬
        readJSONLR(pJSONObject), ¬
        readJSONLR(pJSONillFormed)}
end run

-- GENERIC: ('EITHER' OPTION TYPE) ----------------------------------------

-- Left :: a -> Either a b
on |Left|(x)
    {type:"Either", |Left|:x, |Right|:missing value}
end |Left|

-- Right :: b -> Either a b
on |Right|(x)
    {type:"Either", |Left|:missing value, |Right|:x}
end |Right|

Fine — but you’re now out to a time penalty of more than 30%. Time isn’t everything of course, but there comes a point where shoe-horning a square peg into a round hole starts taking its toll.

The question, of course, is simply whether, in a particular context, it is more economic to optimize for run-time or for development time.

( Time spent staring into a debugger, or waiting for a pick-up truck, is also time (no matter how fast we were going when the incident occurred :slight_smile: ))

Yep. There’s quite a difference in writing scripts for one’s own occasional use and writing scripts that will be deployed to multiple users for regular use. I plead guilty to a bias towards to the latter, simply because I’ve done more of it. And in the publishing industry time taken often matters more in terms of meeting deadlines than saving labor.

Quite understood – that clearly shifts the relative price of development time and run-time.

I just sling things together for my own work – only economic if I can:

  • do it rapidly,
  • with high levels of code reuse,
  • and with patterns of composition that are simple and well-tested enough to usually work first time.

I do appreciate the motivating joys of speed (Honda was built from Mr H’s passion for racing and souping things up) but it would usually be a frivolous thing for me to spend time on. (And even Mr Honda wouldn’t have made it without the restraining influence of a serious-minded business partner).