AppleScript Record to JSON

I’ve got a project that will provide spec files to handle tag translations between two proprietary systems.

We were going to do this in xml, but the developer would prefer JSON.

I have the data in AppleScript records, is there a good solution to go from AppleScript records to JSON (and back again)?

Here’s an old library I’ve used:

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

-- pass a string, list, record or number, and either a path to save the result to, or missing value to have it returned as text
on convertASToJSON:someASThing saveTo:posixPath
	--convert to JSON data
	set {theData, theError} to current application's NSJSONSerialization's dataWithJSONObject:someASThing options:0 |error|:(reference)
	if theData is missing value then error (theError's localizedDescription() as text) number -10000
	if posixPath is missing value then -- return string
		-- convert data to a UTF8 string
		set someString to current application's NSString's alloc()'s initWithData:theData encoding:(current application's NSUTF8StringEncoding)
		return someString as text
	else
		-- write data to file
		set theResult to theData's writeToFile:posixPath atomically:true
		return theResult as boolean -- returns false if save failed
	end if
end convertASToJSON:saveTo:

-- pass either a POSIX path to the JSON file, or a JSON string; isPath is a boolean value to tell which
on convertJSONToAS:jsonStringOrPath isPath:isPath
	if isPath then -- read file as data
		set theData to current application's NSData's dataWithContentsOfFile:jsonStringOrPath
	else -- it's a string, convert to data
		set aString to current application's NSString's stringWithString:jsonStringOrPath
		set theData to aString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
	end if
	-- convert to Cocoa object
	set {theThing, theError} to current application's NSJSONSerialization's JSONObjectWithData:theData options:0 |error|:(reference)
	if theThing is missing value then error (theError's localizedDescription() as text) number -10000
	-- we don't know the class of theThing for coercion, so...
	set listOfThing to current application's NSArray's arrayWithObject:theThing
	return item 1 of (theThing as list)
end convertJSONToAS:isPath:

-- pass a string, list, record or number, and either a path to save the result to, or missing value to have it returned as text
on convertASToPlist:someASThing saveTo:posixPath
	if posixPath is missing value then -- return string
		-- convert to property list data
		set {theData, theError} to current application's NSPropertyListSerialization's dataWithPropertyList:someASThing |format|:(current application's NSPropertyListXMLFormat_v1_0) options:0 |error|:(reference) -- don't use binary format
		if theData is missing value then error (theError's localizedDescription() as text) number -10000
		-- convert data to UTF8 string
		set someString to current application's NSString's alloc()'s initWithData:theData encoding:(current application's NSUTF8StringEncoding)
		return someString as text
	else -- saving to file
		-- convert to property list data
		set {theData, theError} to current application's NSPropertyListSerialization's dataWithPropertyList:someASThing |format|:(current application's NSPropertyListBinaryFormat_v1_0) options:0 |error|:(reference) -- might as well use binary format
		if theData is missing value then error (theError's localizedDescription() as text) number -10000
		-- write data to file
		set theResult to theData's writeToFile:posixPath atomically:true
		return theResult as boolean -- returns false if save failed
	end if
end convertASToPlist:saveTo:

-- pass either a POSIX path to the .plist file, or a property list string; isPath is a boolean value to tell which
on convertPlistToAS:plistStringOrPath isPath:isPath
	if isPath then -- read file as data
		set theData to current application's NSData's dataWithContentsOfFile:plistStringOrPath
	else -- it's a string, convert to data
		set aString to current application's NSString's stringWithString:plistStringOrPath
		set theData to aString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
	end if
	-- convert to Cocoa object
	set {theThing, theError} to current application's NSPropertyListSerialization's propertyListWithData:theData options:0 |format|:(missing value) |error|:(reference)
	if theThing is missing value then error (theError's localizedDescription() as text) number -10000
	-- we don't know the class of theThing for coercion, so...
	set listOfThing to current application's NSArray's arrayWithObject:theThing
	return item 1 of (theThing as list)
end convertPlistToAS:isPath:

-- pass either a POSIX path to the JSON file, or a JSON string; isPath is a boolean value to tell which. saveTo is either a path to save the result to, or missing value to have it returned as text
on convertJSONToPlist:jsonStringOrPath isPath:isPath saveTo:posixPath
	if isPath then -- read file as data
		set theData to current application's NSData's dataWithContentsOfFile:jsonStringOrPath
	else -- it's a string, convert to data
		set aString to current application's NSString's stringWithString:jsonStringOrPath
		set theData to aString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
	end if
	-- convert to Cocoa object
	set {theThing, theError} to current application's NSJSONSerialization's JSONObjectWithData:theData options:0 |error|:(reference)
	if theThing is missing value then error (theError's localizedDescription() as text) number -10000
	if posixPath is missing value then -- return string
		-- convert to property list data
		set {theData, theError} to current application's NSPropertyListSerialization's dataWithPropertyList:theThing |format|:(current application's NSPropertyListXMLFormat_v1_0) options:0 |error|:(reference) -- don't use binary format
		if theData is missing value then error (theError's localizedDescription() as text) number -10000
		-- convert data to UTF8 string
		set someString to current application's NSString's alloc()'s initWithData:theData encoding:(current application's NSUTF8StringEncoding)
		return someString as text
	else -- saving to file
		-- convert to property list data
		set {theData, theError} to current application's NSPropertyListSerialization's dataWithPropertyList:theThing |format|:(current application's NSPropertyListBinaryFormat_v1_0) options:0 |error|:(reference)
		if theData is missing value then error (theError's localizedDescription() as text) number -10000
		-- write data to file
		set theResult to theData's writeToFile:posixPath atomically:true
		return theResult as boolean -- returns false if save failed
	end if
end convertJSONToPlist:isPath:saveTo:

-- pass either a POSIX path to the .plist file, or a property list string; isPath is a boolean value to tell which. saveTo is either a path to save the result to, or missing value to have it returned as text
on convertPlistToJSON:plistStringOrPath isPath:isPath saveTo:posixPath
	if isPath then -- read file as data
		set theData to current application's NSData's dataWithContentsOfFile:plistStringOrPath
	else -- it's a string, convert to data
		set aString to current application's NSString's stringWithString:plistStringOrPath
		set theData to aString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
	end if
	-- convert to Cocoa object
	set {theThing, theError} to current application's NSPropertyListSerialization's propertyListWithData:theData options:0 |format|:(missing value) |error|:(reference)
	if theThing is missing value then error (theError's localizedDescription() as text) number -10000
	--convert to JSON data
	set {theData, theError} to current application's NSJSONSerialization's dataWithJSONObject:theThing options:0 |error|:(reference)
	if theData is missing value then error (theError's localizedDescription() as text) number -10000
	if posixPath is missing value then -- return string
		-- convert data to a UTF8 string
		set someString to current application's NSString's alloc()'s initWithData:theData encoding:(current application's NSUTF8StringEncoding)
		return someString as text
	else
		-- write data to file
		set theResult to theData's writeToFile:posixPath atomically:true
		return theResult as boolean -- returns false if save failed	
	end if
end convertPlistToJSON:isPath:saveTo:

Thanks, Shane!

Is there something about JSON that makes it want to change the order of the AS Record it gets?

I saw on AS Users a version of this from a couple years ago with a pretty print option, is that still an option?

Never mind about the pretty print, I think I got that. There was also a question bout if the / needed to be escaped. Is that required?

So, here’s the issue. The data are tag replacements and they have to be done in a specific order.

"<p><strong>"

has to be replaced before

"<strong>" 

or it doesn’t work. But this seems to change the order in the JSON output. Is that something we can control?

{preBodyHeader:{"<head>", "DANCE", "</head>", "<head_deck>", "</head_deck>", "<byline>", "</byline>", "<body>", "<l_info>", "<roman>", "Compiled by", "</roman>", " Matt Cooper<EP>", "</l_info>"}, openBodyText:"<body>", firstH3Tag:{"<h3>", "</h3>", "<l_leadin>", "</l_leadin>"}, h3Tag:{"<h3>", "</h3>", "<vb_6p><l_leadin>", "</l_leadin>"}, firstPStrongTag:{"<p><strong>", "</strong>", "<NO,,><vb_6p><NO><l_leadin>", "</l_leadin>"}, pStrongTag:{"<p><strong>", "</strong>", "<vb_6p><l_leadin>", "</l_leadin>"}, strongTag:{"<strong>", "</strong>", "<bold>", "</bold>"}, firstPemTag:{"<p><em>", "</em>", "<EP><italic>", "</italic>"}, pemTag:{"<p><em>", "</em>", "<EP><italic>", "</italic>"}, emTag:{"<em>", "</em>", "<italic>", "</italic>"}, inBodyFooter:"", closeBodyTag:"</body>", postBodyFooter:""}

I get this JSON

{
  "slug" : "la-ca-list-0723-dance",
  "tags" : [
    {
      "firstPStrongTag" : [
        "<p><strong>",
        "<\/strong>",
        "<NO,,><vb_6p><NO><l_leadin>",
        "<\/l_leadin>"
      ],
      "strongTag" : [
        "<strong>",
        "<\/strong>",
        "<bold>",
        "<\/bold>"
      ],
      "emTag" : [
        "<em>",
        "<\/em>",
        "<italic>",
        "<\/italic>"
      ],
      "h3Tag" : [
        "<h3>",
        "<\/h3>",
        "<vb_6p><l_leadin>",
        "<\/l_leadin>"
      ],
      "pemTag" : [
        "<p><em>",
        "<\/em>",
        "<EP><italic>",
        "<\/italic>"
      ],
      "inBodyFooter" : "",
      "pStrongTag" : [
        "<p><strong>",
        "<\/strong>",
        "<vb_6p><l_leadin>",
        "<\/l_leadin>"
      ],
      "firstPemTag" : [
        "<p><em>",
        "<\/em>",
        "<EP><italic>",
        "<\/italic>"
      ],
      "preBodyHeader" : [
        "<head>",
        "DANCE",
        "<\/head>",
        "<head_deck>",
        "<\/head_deck>",
        "<byline>",
        "<\/byline>",
        "<body>",
        "<l_info>",
        "<roman>",
        "Compiled by",
        "<\/roman>",
        " Matt Cooper<EP>",
        "<\/l_info>"
      ],
      "closeBodyTag" : "<\/body>",
      "firstH3Tag" : [
        "<h3>",
        "<\/h3>",
        "<l_leadin>",
        "<\/l_leadin>"
      ],
      "postBodyFooter" : "",
      "openBodyText" : "<body>"
    }
  ]
}

Records don’t strictly have any order – they’re labelled, rather than ordered, collections. The fact that they keep their order in AppleScript is just an artefact of how (badly) they’re implemented.

And just to expand, this is nothing to do with JSON. If you run this:

set aDict to current application's NSDictionary's dictionaryWithDictionary:x

where x is your record, you’ll see the same order. In other words, it’s the scripting bridge’s conversion from record to dictionary that alters order.

But the ASLG is pretty clear: “A record is an unordered collection of labeled properties.”

So, for the JSON data, is there a way to reorder, or is that not something to worry about since it’s labeled?

Ed, are you open to using JavaScript for Automation (JXA)?
Handling of JSON data is easy and built-in to JavaScript.

Of course, whatever your source is for the data, you can read it just as easily with JXA as you can AppleScript, maybe even easier.

I use it often.

1 Like

This ^. It’s just another way of representing key-value pairs.

1 Like

JXA may be something I have to learn. The source for the information will be Google Sheets. (Currently I’m copying and pasting into text wrangler and then running the AppleScript.)

Then I think your job could be much easier.
Looks like it is easy to export Google Sheets to JSON.
Do a Google search on “google sheets save as json” and you see lots of solutions, including this one:
Export Sheet Data, which is a Google Chrome extension.

If you like to try, I’d be glad to help you.

May I suggest that we use the new JXA sub-group of the Groups.io Apple-Dev:
https://apple-dev.groups.io/g/jxa

Here is a good resource to get started with JXA:
JXA Resources

IMO, JXA handles lists (arrays) and records (objects) much much better than native AppleScript. JXA also has great string functions and a great, easy-to-use RegEx engine.

“…is just an artifact of how (badly) they’re implemented”

Thanks for the laugh Shane! :~) Don’t know why that struck me so blamed funny.

Thanks so much for sharing the library. I’m familiar with general AppleScript but not the foundation framework. Is there a way to modify the ASToJSON handler to append the data to a JSON file in the saveTo path instead of replacing all of the data in that file?

There is — but it would probably be easier to just get the JSON as text and append that using the standard additions write command. That said, this will append:

on convertASToJSON:someASThing appendingTo:posixPath
	--convert to JSON data
	set {theData, theError} to current application's NSJSONSerialization's dataWithJSONObject:someASThing options:0 |error|:(reference)
	if theData is missing value then error (theError's localizedDescription() as text) number -10000
	-- create file if it doesn't exist
	set fileManager to current application's NSFileManager's defaultManager()
	if not (fileManager's fileExistsAtPath:posixPath) as boolean then
		fileManager's createFileAtPath:posixPath |contents|:(missing value) attributes:(missing value)
	end if
	-- write to file via file handle
	set fh to current application's NSFileHandle's fileHandleForWritingAtPath:posixPath
	fh's seekToEndOfFile()
	fh's writeData:theData
	fh's closeFile()
end convertASToJSON:appendingTo:
1 Like