-
Notifications
You must be signed in to change notification settings - Fork 851
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow JSON.stringify to operate on Java objects #860
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
load('testsrc/assert.js'); | ||
|
||
function replacer(key, value) { | ||
var javatype = value instanceof java.lang.Object ? value.getClass().name : null; | ||
return javatype ? 'replaced: ' + javatype : value; | ||
} | ||
|
||
// java.lang.String | ||
var javaString = new java.lang.String('test'); | ||
|
||
var obj = {test: javaString}; | ||
var expected = JSON.stringify({test: 'replaced: java.lang.String'}); | ||
var actual = JSON.stringify(obj, replacer); | ||
assertEquals(expected, actual); | ||
|
||
var obj = {test: javaString}; | ||
var expected = JSON.stringify({test: 'test'}); | ||
var actual = JSON.stringify(obj); | ||
assertEquals(expected, actual); | ||
|
||
// java.lang.Double | ||
var javaDouble = java.lang.Double.valueOf(12.34); | ||
|
||
var obj = {test: javaDouble}; | ||
var expected = JSON.stringify({test: 'replaced: java.lang.Double'}); | ||
var actual = JSON.stringify(obj, replacer); | ||
assertEquals(expected, actual); | ||
|
||
var obj = {test: javaDouble}; | ||
var expected = JSON.stringify({test: 12.34}); | ||
var actual = JSON.stringify(obj); | ||
assertEquals(expected, actual); | ||
|
||
// java.lang.Boolean | ||
var javaBoolean = java.lang.Boolean.valueOf(false); | ||
|
||
var obj = {test: javaBoolean}; | ||
var expected = JSON.stringify({test: 'replaced: java.lang.Boolean'}); | ||
var actual = JSON.stringify(obj, replacer); | ||
assertEquals(expected, actual); | ||
|
||
var obj = {test: javaBoolean}; | ||
var expected = JSON.stringify({test: false}); | ||
var actual = JSON.stringify(obj); | ||
assertEquals(expected, actual); | ||
|
||
// java.util.Collection | ||
var javaCollection = new java.util.LinkedHashSet(); | ||
javaCollection.add('test'); | ||
javaCollection.add({nested: 'jsObj'}); | ||
|
||
var obj = {test: javaCollection}; | ||
var expected = JSON.stringify({test: 'replaced: java.util.LinkedHashSet'}); | ||
var actual = JSON.stringify(obj, replacer); | ||
assertEquals(expected, actual); | ||
|
||
var obj = {test: javaCollection}; | ||
var expected = JSON.stringify({test: ['test', {nested: 'jsObj'}]}); | ||
var actual = JSON.stringify(obj); | ||
assertEquals(expected, actual); | ||
|
||
// java Array | ||
var javaArray = new java.lang.String('a,b,c').split(','); | ||
|
||
var obj = {test: javaArray}; | ||
var expected = JSON.stringify({test: 'replaced: [Ljava.lang.String;'}); | ||
var actual = JSON.stringify(obj, replacer); | ||
assertEquals(expected, actual); | ||
|
||
var obj = {test: javaArray}; | ||
var expected = JSON.stringify({test: ['a','b','c']}); | ||
var actual = JSON.stringify(obj); | ||
assertEquals(expected, actual); | ||
|
||
// java Map | ||
var javaMap = new java.util.HashMap(); | ||
javaMap.put(new java.lang.Object(), 'property skipped if key is not string-like'); | ||
javaMap.put('te' + 'st', 55); | ||
|
||
var obj = {test: javaMap}; | ||
var expected = JSON.stringify({test: 'replaced: java.util.HashMap'}); | ||
var actual = JSON.stringify(obj, replacer); | ||
assertEquals(expected, actual); | ||
|
||
var obj = javaMap; | ||
var expected = JSON.stringify({test: 55}); | ||
var actual = JSON.stringify(obj); | ||
assertEquals(expected, actual); | ||
|
||
// complex object | ||
var obj = { | ||
array: javaArray, | ||
boxed: [javaDouble, javaBoolean], | ||
objects: { | ||
plainJS: {test: 1}, | ||
emptyMap: java.util.Collections.EMPTY_MAP, | ||
otherMap: javaMap | ||
} | ||
}; | ||
var expected = JSON.stringify({ | ||
array: ['a','b','c'], | ||
boxed: [12.34, false], | ||
objects: { | ||
plainJS: {test: 1}, | ||
emptyMap: {}, | ||
otherMap: {test: 55} | ||
} | ||
}); | ||
var actual = JSON.stringify(obj); | ||
assertEquals(expected, actual); | ||
|
||
// other Java object | ||
var javaObject = new java.net.URI('test://other/java/object'); | ||
|
||
var obj = {test: javaObject}; | ||
var expected = JSON.stringify({test: 'replaced: java.net.URI'}); | ||
var actual = JSON.stringify(obj, replacer); | ||
assertEquals(expected, actual); | ||
|
||
var obj = {test: javaObject}; | ||
assertThrows(()=>JSON.stringify(obj), TypeError); | ||
|
||
// JavaAdapter with toJSON | ||
var javaObject = new JavaAdapter(java.lang.Object, { | ||
toJSON: _ => ({javaAdapter: true}) | ||
}); | ||
|
||
var obj = javaObject; | ||
var expected = JSON.stringify({javaAdapter: true}); | ||
var actual = JSON.stringify(obj, replacer); | ||
assertEquals(expected, actual); | ||
|
||
// JavaAdapter without toJSON | ||
var javaObject = new JavaAdapter(java.lang.Object, { | ||
toString: () => 'just an object' | ||
}); | ||
|
||
var obj = {test: javaObject}; | ||
var expected = /^replaced: adapter\d+$/; | ||
var actual = JSON.parse(JSON.stringify(obj, replacer)).test; | ||
assertEquals("string", typeof actual); | ||
assertTrue(expected.test(actual)); | ||
|
||
var obj = {test: javaObject}; | ||
assertThrows(()=>JSON.stringify(obj), TypeError); | ||
|
||
"success" |
10 changes: 10 additions & 0 deletions
10
testsrc/org/mozilla/javascript/tests/StringifyJavaObjectsTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package org.mozilla.javascript.tests; | ||
|
||
import org.mozilla.javascript.Context; | ||
import org.mozilla.javascript.drivers.LanguageVersion; | ||
import org.mozilla.javascript.drivers.RhinoTest; | ||
import org.mozilla.javascript.drivers.ScriptTestsBase; | ||
|
||
@RhinoTest("testsrc/jstests/stringify-java-objects.js") | ||
@LanguageVersion(Context.VERSION_ES6) | ||
public class StringifyJavaObjectsTest extends ScriptTestsBase {} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm worried that throwing this exception will break backward compatibility. Previously, someone with a complex object who is trying to stringify it might end up with a weird result, and now they will get a less weird result. But if there's a chance that people who were previously getting weird results will now get an exception, then we could easily break a lot of code. Unless the ECMA spec says that we should throw an exception in this case, then do we really need it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current code sees it as a NativeJavaObject, which is Scriptable, and so it sends it down the path to try to stringify it as a javascript object, because it is not Callable. This is throwing the StackOverflowError mentioned in the linked issue. I believe the ECMA spec says that all Objects that are not Functions should throw a TypeError if they are cyclic or return a JSON string representing an object.
Since I am unwrapping the NativeJavaObject, it is no longer Scriptable. I think at this point it either has to be Undefined or some other Java Object that can't be serialized, so the options are to either throw a more descriptive exception than it had been throwing, return Undefined, or return "
{}
".I believe prior to 4c76ef2 it never made it this far and threw the other exception described in the linked issue:
That commit let the object make it through to the replacer, but created a worse exception if a replacer did not exist to do something with the object.
I think my TypeError is in line with the historical behavior before the StackOverflowError, but I am open to suggestions. This also mimics the behavior of BigInt, which throws a TypeError when you try to stringify it without a replacer or adding a custom BigInt.prototype.toJSON method.