Custom JSON Parsing with Reviver Functions
The built-in JSON
class that debuted with Flash Player 11 has an interesting feature that few AS3 programmers know about. It turns out that JSON.parse
doesn’t just take the JSON document to parse but also a “reviver” Function
. What is this? How can it be used? Find out more in today’s article and take advantage of this powerful parsing option.
According to the documentation, the optional “reviver” parameter to JSON.parse
is as follows:
The reviver parameter is a function that takes two parameters: a key and a value. You can use this function to transform or filter each key/value pair as it is parsed. If you supply a reviver function, your transformed or filtered value for each pair, rather than the default parsing, is returned in the parse() function output. If the reviver function returns undefined for any pair, the property is deleted from the final result.
Now let’s take a look at a tiny app that demonstrates kinds of calls your reviver function can expect:
package { import flash.display.Sprite; import flash.events.Event; import flash.text.TextField; import flash.text.TextFormat; import flash.utils.describeType; public class ReviverFunction extends Sprite { public function ReviverFunction() { var logger:TextField = new TextField(); logger.defaultTextFormat = new TextFormat("_typewriter", 10); logger.width = stage.stageWidth; logger.height = stage.stageHeight; addChild(logger); function log(msg:String): void { logger.appendText(msg+"\n"); } var str:String = '{' + '"students":[' + '{"first":"Sara", "last":"Smith"},' + '{"first":"Mike", "last":"Thompson"}' + '],' + '"teachers":[' + '{"first":"Greg", "last":"Miller"},' + '{"first":"Kara", "last":"Hernandez"}' + ']' + '}'; var obj:Object = JSON.parse( str, function(key:String, val:Object): * { if (val is String) { log('"' + key + '" = "' + val + '"'); } else if (val is Array) { log('"' + key + '" = ['); var arr:Array = val as Array; log("LENGTH: " + arr.length); for (var i:int = 0; i < arr.length; ++i) { var cur:* = arr[i]; log(" " + cur); } log("]"); } else { log('"' + key + '" = {'); for (var k:* in val) { log(" " + key + ": " + val[k]); } log("}"); } } ); } } }
Here is the output of this program:
"first" = "Sara" "last" = "Smith" "0" = { } "first" = "Mike" "last" = "Thompson" "1" = { } "students" = [ LENGTH: 2 undefined undefined ] "first" = "Greg" "last" = "Miller" "0" = { } "first" = "Kara" "last" = "Hernandez" "1" = { } "teachers" = [ LENGTH: 2 undefined undefined ] "" = { }
Several aspects of this output are strange, so it may not be what you were expecting to see. For example, the elements of the arrays are passed to the reviver function with String
keys for their indices rather than integers. Since AS3 automatically converts between strings and indices this may not be much of a problem, but it’s a bit odd.
Much more strange is that these arrays have the appropriate lengths but their contents are completely undefined. For example, the “teachers” array has the appropriate length of two but the elements at index 0 and 1 are both undefined
.
Similarly, all objects (e.g. the students) have no elements within them. It’s not shown in the above source code, but if you try to manually access the fields (e.g. val["first"]
or val.first
) you’ll still get undefined
.
This means that the only interesting values your reviver function gets are of type String
. It may still be interesting to know how long an Array
is or that something is an Object
but the contents of those types will remain a mystery to your reviver function.
Armed with that information, what can we do with the reviver function? Well, we could follow the documentation’s recommendation and transform the input. Here’s a case where we capitalize the students’ and teachers’ names:
package { import flash.display.Sprite; import flash.events.Event; import flash.text.TextField; import flash.text.TextFormat; import flash.utils.describeType; public class ReviverFunctionUpper extends Sprite { public function ReviverFunctionUpper() { var logger:TextField = new TextField(); logger.defaultTextFormat = new TextFormat("_typewriter", 10); logger.width = stage.stageWidth; logger.height = stage.stageHeight; addChild(logger); function log(msg:String): void { logger.appendText(msg+"\n"); } var str:String = '{' + '"students":[' + '{"first":"sara", "last":"smith"},' + '{"first":"mike", "last":"thompson"}' + '],' + '"teachers":[' + '{"first":"greg", "last":"miller"},' + '{"first":"kara", "last":"hernandez"}' + ']' + '}'; var obj:Object = JSON.parse( str, function(key:String, val:Object): * { if (key == "first" || key == "last") { return val.charAt(0).toUpperCase() + val.substr(1); } return val; } ); for each (var person:Object in obj.students.concat(obj.teachers)) { log(person.first + " " + person.last); } } } }
Output:
Sara Smith Mike Thompson Greg Miller Kara Hernandez
Or you could filter out unwanted values. Here’s a case where a maximum last name length is set to 5 and everybody else is rejected:
package { import flash.display.Sprite; import flash.events.Event; import flash.text.TextField; import flash.text.TextFormat; import flash.utils.describeType; public class ReviverFunctionShort extends Sprite { public function ReviverFunctionShort() { var logger:TextField = new TextField(); logger.defaultTextFormat = new TextFormat("_typewriter", 10); logger.width = stage.stageWidth; logger.height = stage.stageHeight; addChild(logger); function log(msg:String): void { logger.appendText(msg+"\n"); } var str:String = '{' + '"students":[' + '{"first":"Sara", "last":"Smith"},' + '{"first":"Mike", "last":"Thompson"}' + '],' + '"teachers":[' + '{"first":"Greg", "last":"Miller"},' + '{"first":"Kara", "last":"Hernandez"}' + ']' + '}'; var obj:Object = JSON.parse( str, function(key:String, val:Object): * { if ((key == "first" || key == "last") && val.length > 5) { return undefined; } return val; } ); for each (var person:Object in obj.students.concat(obj.teachers)) { log(person.first + " " + person.last); } } } }
Output:
Sara Smith Mike undefined Greg undefined Kara undefined
Well that didn’t really work. How could we exclude these long-named students? Unfortunately, the reviver function isn’t really up the task in this case. We’d need to restructure our JSON document to accommodate for the strict, contextless key-value pair nature of the reviver function. In this case we merge the names into one field so we can reject them all at once rather than rejecting just the first or last name:
package { import flash.display.Sprite; import flash.events.Event; import flash.text.TextField; import flash.text.TextFormat; import flash.utils.describeType; public class ReviverFunctionShort2 extends Sprite { public function ReviverFunctionShort2() { var logger:TextField = new TextField(); logger.defaultTextFormat = new TextFormat("_typewriter", 10); logger.width = stage.stageWidth; logger.height = stage.stageHeight; addChild(logger); function log(msg:String): void { logger.appendText(msg+"\n"); } var str:String = '{' + '"students":[' + '"name:Sara_Smith",' + '"name:Mike_Thompson"' + '],' + '"teachers":[' + '"name:Greg_Miller",' + '"name:Kara_Hernandez"' + ']' + '}'; var obj:Object = JSON.parse( str, function(key:String, val:Object): * { if (val is String && val.indexOf("name:") == 0) { var name:String = val.substr("name:".length); var parts:Array = name.split("_"); if (parts[1].length > 5) { return undefined; } return {first:parts[0], last:parts[1]}; } return val; } ); for each (var person:Object in obj.students.concat(obj.teachers)) { log(person.first + " " + person.last); } } } }
Output:
Sara Smith
This allows us to get the desired result, but now we’ve written a lot more parsing code and lost many of the benefits of the automatic parsing that we get with the JSON
parser. Still, it’s a way to do some light filtering of the JSON data. If you want to do something more complex like parse the JSON document to an instance of some AS3 class you have, Adobe has the gory details. It’s not for the feint of heart.
As an alternative, one that isn’t human-readable or supported by as many apps but it much better compressed, consider AMF as discussed in these articles:
- Serialize Anything
- Serialize Anything: Part 2
- 450x Speed Up Copying Between Collections
- Why Objects Serialized With ByteArray Are So Small
- AMF Serialization Tricks
Spot a bug? Have a question or suggestion? Post a comment!
#1 by Matthew Wilcoxson on October 27th, 2017 ·
Hi,
The reason you are getting strange outputs in the first run is that that function has no return. You are just replacing all the values with undefined which is why the objects and arrays appear to be empty.
Cheers
Mat