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()"
-- }
-- }
-- }
-- }
--}
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%.
(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.
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).