AppleScript Record to JSON

asobjc

(Ed Stockly) #1

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


(Shane Stanley) #2

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:

(Ed Stockly) #3

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?


(Ed Stockly) #4

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?


(Ed Stockly) #5

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>"
    }
  ]
}

(Shane Stanley) #6

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


(Ed Stockly) #7

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


(Jim Underwood) #8

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.


(Shane Stanley) #9

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


(Ed Stockly) #10

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


(Jim Underwood) #11

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.


(Rick Pepper) #12

“…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.