JSON support for NSDictionary

152 views
Skip to first unread message

mugginsoft

unread,
Apr 24, 2012, 8:09:37 AM4/24/12
to jsc...@googlegroups.com
JSON can be obtained as expected for objects containing native JS types:
var theItem = {where:"here", who:"you"}
return JSON.stringify(theItem)

When our object contains Obj-C objects, or the container is an Obj-C object then it raises:

var theItem = {where:@"here", who:@"you"}
return JSON.stringify(theItem)

var theItem = [NSDictionary dictionaryWithObjectsAndKeys: @"here", @"where", @"you", @"who", nil]
return JSON.stringify(theItem)

JSException: jsCocoaObject_callAsFunction : method toJSON of object __NSCFString not found — remnant of a split call


Converting to JS also fails:


var theItem = [NSDictionary dictionaryWithObjectsAndKeys: @"here", @"where", @"you", @"who", nil]
return JSON.stringify(__jsc__.toJS(theItem)) // result is an empty array

Calling .toJS() works if the container object is an NSArray but not if it is an NSDictionary instance.

The reason for this is that .toJS converts NSDictionary into a JS array and adds the keys as named properties.

JSON.stringify() omits named properties when serialising a JS array.

Named properties are correctly represented only for objects.


To me it would seem that an object rather than an array with named properties is a better fit as an NSDictionary equivalent but changing it may break existing code.






Patrick Geiller

unread,
Apr 25, 2012, 2:50:24 PM4/25/12
to jsc...@googlegroups.com
var theItem = [NSDictionary dictionaryWithObjectsAndKeys: @"here", @"where", @"you", @"who", nil]
return JSON.stringify(theItem)

JSException: jsCocoaObject_callAsFunction : method toJSON of object __NSCFString not found — remnant of a split call


Easy fix :

String.prototype.toJSON = function () { return this }

JSON.stringify is a native method and therefore handles strings natively. For other objects (like NSDate), it looks for a toJSON method.
Since strings are handled natively, Javascript strings don't have the toJSON method, so just add it.



BTW — when handling NSStrings, JSCocoa uses the String.prototype if it can't find a matching NSString method :

var nsstring = [NSString stringWithString:'a/b/c'] // Create a NSString …
nsstring.split('/') // … and call a JS method on it

Jonathan Mitchell

unread,
Apr 25, 2012, 6:17:09 PM4/25/12
to jsc...@googlegroups.com
Thanks for the explanation. I was unaware of the existence of JavaScript prototypes.

For the sample quoted the fix does indeed work. It's a neat trick.
But as you have noted non native objects such as NSNumber spoil the party.

var theNumber = [NSNumber numberWithInteger:100]
var theNSDict = [NSDictionary dictionaryWithObjectsAndKeys:@"Hello", @"brave", theNumber, @"new"]
return JSON.stringify(theNSDict)

>> {"brave":"Hello","new":{}}

A general solution might be to define toJSON as Obj-C category methods on the property list types.
The actual conversion could be accomplished using NSJSONSerialization (10.7 only)
Other 3rd party JSON libraries are of course available.

Regards

Jonathan



Jonathan Mitchell

unread,
Apr 25, 2012, 6:42:51 PM4/25/12
to jsc...@googlegroups.com

On 25 Apr 2012, at 23:17, Jonathan Mitchell wrote:

>
> On 25 Apr 2012, at 19:50, Patrick Geiller wrote:
>
>> Easy fix :
>>
>> String.prototype.toJSON = function () { return this }
>>
>> JSON.stringify is a native method and therefore handles strings natively. For other objects (like NSDate), it looks for a toJSON method.
>> Since strings are handled natively, Javascript strings don't have the toJSON method, so just add it.
>>
>>
>
> For the sample quoted the fix does indeed work. It's a neat trick.
> But as you have noted non native objects such as NSNumber spoil the party.
>
> var theNumber = [NSNumber numberWithInteger:100]
> var theNSDict = [NSDictionary dictionaryWithObjectsAndKeys:@"Hello", @"brave", theNumber, @"new"]
> return JSON.stringify(theNSDict)
>
>>> {"brave":"Hello","new":{}}
>
>
A work around for this has been posted to https://github.com/mugginsoft/jscocoa
A controller method toJSObject has been added that generates a more usable NSDictionary representation.
Direct conversion of NSObject subclasses to JSON is still not supported.

// get the JSCocoa controller.
// this is also always available as __jsc__
var jsc = JSCocoaController.sharedController

// define an NSDictionary instance
var theNSDict = [NSDictionary dictionaryWithObjectsAndKeys:@"Hello", @"brave", @"new", @"kosmos"]

// convert to JS dict
var theJSDict = jsc.toJSObject(theNSDict)
log(JSON.stringify(theJSDict))

var theNSDict2 = [NSDictionary dictionaryWithObjectsAndKeys:@"Hello", @"brave", [NSNumber numberWithInteger:100], @"kosmos", [NSDate date], @"now"]
//var theJSDict2 = jsc.toJS(theNSDict2) // this gives an empty dictionary
var theJSDict2 = jsc.toJSObject(theNSDict2)
log(JSON.stringify(theJSDict2))

var theNSArray = [NSArray arrayWithObjects: [NSNumber numberWithInteger:100], [NSDate date], theNSDict]
var theNSDict3 = [NSDictionary dictionaryWithObjectsAndKeys:theNSDict, @"dict1", theNSDict2, @"dict2", theNSArray, @"array"]
var theJSDict3 = jsc.toJSObject(theNSDict3)

// stringify the JS dict
return JSON.stringify(theJSDict3)

>>{"dict1":{"brave":"Hello","kosmos":"new"},"dict2":{"brave":"Hello","now":"2012-04-25T22:40:08.913Z","kosmos":100},"array":[100,"2012-04-25T22:40:08.922Z",{"brave":"Hello","kosmos":"new"}]}

Both toJS: and toJSObject raise if passed a non NSString dictionary key.
This is not such a bit deal as keys are normally strings (though not exclusively).
Keys in property lists however must be NSString instances.

regards

Jonathan Mitchell







Patrick Geiller

unread,
Apr 26, 2012, 1:51:42 PM4/26/12
to jsc...@googlegroups.com
var theNSDict2 = [NSDictionary dictionaryWithObjectsAndKeys:@"Hello", @"brave", [NSNumber numberWithInteger:100], @"kosmos", [NSDate date], @"now"]

You can add js functions to existing ObjC classes, so about :

// This is yesterday's fix
String.prototype.toJSON = function () { return this }

// New methods for native classes, which must return js values and not ObjC objects (Hence .valueOf() and the String() constructor)
class_add_js_function(NSNumber, 'toJSON', function () { return this.valueOf() } )
class_add_js_function(NSDate, 'toJSON', function () { return String(this.description) } )


var theNSDict2 = [NSDictionary dictionaryWithObjectsAndKeys:@"Hello", @"brave", [NSNumber numberWithInteger:100], @"kosmos", [NSDate date], @"now"]

// This gives r={"brave":"Hello","kosmos":100,"now":"2012-04-26 17:47:04 +0000"}
var r = JSON.stringify(theNSDict2)
log('r=' + r)

OK ?

Jonathan Mitchell

unread,
Apr 26, 2012, 4:44:50 PM4/26/12
to jsc...@googlegroups.com

On 26 Apr 2012, at 18:51, Patrick Geiller wrote:

>> var theNSDict2 = [NSDictionary dictionaryWithObjectsAndKeys:@"Hello", @"brave", [NSNumber numberWithInteger:100], @"kosmos", [NSDate date], @"now"]
>
> You can add js functions to existing ObjC classes, so about :
>
> // This is yesterday's fix
> String.prototype.toJSON = function () { return this }
>
> // New methods for native classes, which must return js values and not ObjC objects (Hence .valueOf() and the String() constructor)
> class_add_js_function(NSNumber, 'toJSON', function () { return this.valueOf() } )
> class_add_js_function(NSDate, 'toJSON', function () { return String(this.description) } )
>
That's interesting. There is a lot of power and flexibility in depth in JSCocoa. I think it is a great bridge - thanks to JavaScript's C-like syntax I find it the easiest of the bridges to use.
>
> var theNSDict2 = [NSDictionary dictionaryWithObjectsAndKeys:@"Hello", @"brave", [NSNumber numberWithInteger:100], @"kosmos", [NSDate date], @"now"]
>
> // This gives r={"brave":"Hello","kosmos":100,"now":"2012-04-26 17:47:04 +0000"}
> var r = JSON.stringify(theNSDict2)
> log('r=' + r)
This is progress but I have some questions:

Consider:
function kosmicTask()
{
// This is yesterday's fix
String.prototype.toJSON = function () { return this }

// New methods for native classes, which must return js values and not ObjC objects (Hence .valueOf() and the String() constructor)
class_add_js_function(NSNumber, 'toJSON', function () { return this.valueOf() } )
class_add_js_function(NSDate, 'toJSON', function () { return String(this.description) } )

// JS
var theJSArray = [@"Look", @"up", @"down"]
log("JSON.stringify(theJSArray) " + JSON.stringify(theJSArray)) // GOOD

// NS
var theNSArray = [NSArray arrayWithObjects:@"Look", @"up", @"down", nil]
log("JSON.stringify(theNSArray) " + JSON.stringify(theNSArray)) // BAD output is empty object {}
log("JSON.stringify(__jsc__.toJS(theNSArray)) " + JSON.stringify(__jsc__.toJS(theNSArray))) // GOOD output is correct

var theNSDict = [NSDictionary dictionaryWithObjectsAndKeys:"up", "key1", "down", "key2"]
var theNSDict2 = [NSDictionary dictionaryWithObjectsAndKeys:theNSArray, @"array", theNSDict, @"dict", @"Hello", @"brave", [NSNumber numberWithInteger:100], @"kosmos", [NSDate date], @"now"]

var r = JSON.stringify(theNSDict2) // BAD array value is empty object {}

return r
}


This returns:
{"array":{},"brave":"Hello","now":"2012-04-26 18:34:45 +0000","dict":{"key2":"down","key1":"up"},"kosmos":100}
So I still have some issues with the array.
I don't understand yet how JSCocoa handles the toJSON call for the NSDictionary without it being defined in a similar way.
Where should I look to try and resolve this issue with the array?

The second point is more general.

IMHO it would be much better if the toJSON methods are made globally available without these explicit calls to class_add_js_function in the client.
NSDictionary -> JSON is a pretty fundamental requirement.
They could be tacked on to class.js but a better solution would be a separate json.js resource that gets loaded in the same way.

The above global approach might mean (I'm not quite sure) that the toJSON functions couldn't be then overridden to customise the JSON representation if required.
Another solution might be to provide an enableJSONSupport controller method that explicitly loads json.js.
That would work in my case as I could explicitly configure the JSCocoaController with JSON support before executing the client script.

Regards

Jonathan




Patrick Geiller

unread,
Apr 26, 2012, 5:44:08 PM4/26/12
to jsc...@googlegroups.com
log("JSON.stringify(theNSArray) " + JSON.stringify(theNSArray)) // BAD output is empty object {}
var r = JSON.stringify(theNSDict2) // BAD array value is empty object {}

Both of these work now.

I don't understand yet how JSCocoa handles the toJSON call for the NSDictionary without it being defined in a similar way.

It doesn't specifically, jsCocoaObject_getProperty checks if the ObjC object responds to objectForKey: , and jsCocoaObject_getPropertyNames checks if the ObjC object is a NSDictionary. This lets you use an NSDictionary with Javascript syntax.

Somehow it doesn't work for JSON, I don't know why.

IMHO it would be much better if the toJSON methods are made globally available without these explicit calls to class_add_js_function in the client.
NSDictionary -> JSON is a pretty fundamental requirement.
They could be tacked on to class.js but a better solution would be a separate json.js resource that gets loaded in the same way.

Done, in json.js.
You can load it with

[jsc evalJSFile:[[NSBundle mainBundle] pathForResource:@"json" ofType:@"js"]];


Jonathan Mitchell

unread,
Apr 26, 2012, 7:05:58 PM4/26/12
to jsc...@googlegroups.com
Fantastic work Patrick. Tests as well. Excellent. 
Many thanks. I will rebuild KosmicTask tomorrow with the new commits but they look good in the repo.

I will be pushing a lot of JSON data (originally Plists) from JSCocoa powered tasks into a REST server so the changes will get well exercised then.

The only Plist data type we haven't really considered is NSData (base64 encoded binary), but at least the approach is now well established.


Jonathan Mitchell

unread,
Apr 27, 2012, 4:02:55 PM4/27/12
to jsc...@googlegroups.com
On 26 Apr 2012, at 22:44, Patrick Geiller wrote:

IMHO it would be much better if the toJSON methods are made globally available without these explicit calls to class_add_js_function in the client.
NSDictionary -> JSON is a pretty fundamental requirement.
They could be tacked on to class.js but a better solution would be a separate json.js resource that gets loaded in the same way.

Done, in json.js.
You can load it with

[jsc evalJSFile:[[NSBundle mainBundle] pathForResource:@"json" ofType:@"js"]];


json.js hasn't been added to the JSCocoa framework bundle.
Was that your intention or not?

I suppose it depends whether you consider it part of JSCocoa or supplementary.
Personally, I would put it the JSCocoa bundle and load it from there.

Patrick Geiller

unread,
Apr 27, 2012, 5:25:42 PM4/27/12
to jsc...@googlegroups.com
>
> json.js hasn't been added to the JSCocoa framework bundle.
> Was that your intention or not?
>
> I suppose it depends whether you consider it part of JSCocoa or supplementary.
> Personally, I would put it the JSCocoa bundle and load it from there.

Fixed.

Reply all
Reply to author
Forward
0 new messages