Reading JSON data with NSJSONSerialization

how-to
foundation
asobjc
json

(Mark Alldritt) #1

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)


Writing JSON data with NSJSONSerialization
#2

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



(Shane Stanley) #3

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%.


#4

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|

(Shane Stanley) #5

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.


#6

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: ))


(Shane Stanley) #7

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.


#8

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).