You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by jw...@apache.org on 2016/10/23 23:48:07 UTC

[1/2] groovy git commit: JsonGenerator - JSON serialization options (closes #371, closes #433)

Repository: groovy
Updated Branches:
  refs/heads/master 8213bd079 -> 132025997


http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/spec/test/json/JsonTest.groovy
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/spec/test/json/JsonTest.groovy b/subprojects/groovy-json/src/spec/test/json/JsonTest.groovy
index b320939..28067a6 100644
--- a/subprojects/groovy-json/src/spec/test/json/JsonTest.groovy
+++ b/subprojects/groovy-json/src/spec/test/json/JsonTest.groovy
@@ -94,6 +94,74 @@ class JsonTest extends GroovyTestCase {
         '''
     }
 
+    void testJsonOutputWithGenerator() {
+        assertScript '''
+        import groovy.json.*
+
+        // tag::json_output_generator[]
+        class Person {
+            String name
+            String title
+            int age
+            String password
+            Date dob
+            URL favoriteUrl
+        }
+
+        Person person = new Person(name: 'John', title: null, age: 21, password: 'secret',
+                                    dob: Date.parse('yyyy-MM-dd', '1984-12-15'),
+                                    favoriteUrl: new URL('http://groovy-lang.org/'))
+
+        def generator = new JsonGenerator.Options()
+            .excludeNulls()
+            .dateFormat('MM@dd@yyyy')
+            .excludeFieldsByName('age', 'password')
+            .excludeFieldsByType(URL)
+            .build()
+
+        assert generator.toJson(person) == '{"dob":"12@15@1984","name":"John"}'
+        // end::json_output_generator[]
+        '''
+    }
+
+    void testJsonOutputConverter() {
+        assertScript '''
+        import groovy.json.*
+        import static groovy.test.GroovyAssert.shouldFail
+
+        // tag::json_output_converter[]
+        class Person {
+            String name
+            URL favoriteUrl
+        }
+
+        Person person = new Person(name: 'John', favoriteUrl: new URL('http://groovy-lang.org/json.html#_jsonoutput'))
+
+        def generator = new JsonGenerator.Options()
+            .addConverter(URL) { URL u, String key ->
+                if (key == 'favoriteUrl') {
+                    '"' + u.getHost() + '"'
+                } else {
+                    JsonOutput.toJson(u)
+                }
+            }
+            .build()
+
+        assert generator.toJson(person) == '{"favoriteUrl":"groovy-lang.org","name":"John"}'
+
+        // No key available when generating a JSON Array
+        def list = [new URL('http://groovy-lang.org/json.html#_jsonoutput')]
+        assert generator.toJson(list) == '["http://groovy-lang.org/json.html#_jsonoutput"]'
+
+        // First parameter to the converter must match the type for which it is registered
+        shouldFail(IllegalArgumentException) {
+            new JsonGenerator.Options()
+                .addConverter(Date) { Calendar cal -> }
+        }
+        // end::json_output_converter[]
+        '''
+    }
+
     void testPrettyPrint() {
         // tag::pretty_print[]
         def json = JsonOutput.toJson([name: 'John Doe', age: 42])

http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/spec/test/json/StreamingJsonBuilderTest.groovy
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/spec/test/json/StreamingJsonBuilderTest.groovy b/subprojects/groovy-json/src/spec/test/json/StreamingJsonBuilderTest.groovy
index c9bb0fe..7deb2ae 100644
--- a/subprojects/groovy-json/src/spec/test/json/StreamingJsonBuilderTest.groovy
+++ b/subprojects/groovy-json/src/spec/test/json/StreamingJsonBuilderTest.groovy
@@ -18,8 +18,6 @@
  */
 package json
 
-import groovy.util.GroovyTestCase
-
 class StreamingJsonBuilderTest extends GroovyTestCase {
 
     void testStreamingJsonBuilder() {
@@ -72,4 +70,37 @@ class StreamingJsonBuilderTest extends GroovyTestCase {
             // end::json_assert[]
        """
     }
+
+    void testStreamingJsonBuilderWithGenerator() {
+        assertScript '''
+            import groovy.json.*
+            // tag::streaming_json_builder_generator[]
+            def generator = new JsonGenerator.Options()
+                    .excludeNulls()
+                    .excludeFieldsByName('make', 'country', 'record')
+                    .excludeFieldsByType(Number)
+                    .addConverter(URL) { url -> '"http://groovy-lang.org"' }
+                    .build()
+
+            StringWriter writer = new StringWriter()
+            StreamingJsonBuilder builder = new StreamingJsonBuilder(writer, generator)
+
+            builder.records {
+              car {
+                    name 'HSV Maloo'
+                    make 'Holden'
+                    year 2006
+                    country 'Australia'
+                    homepage new URL('http://example.org')
+                    record {
+                        type 'speed'
+                        description 'production pickup truck with speed of 271kph'
+                    }
+              }
+            }
+
+            assert writer.toString() == '{"records":{"car":{"name":"HSV Maloo","homepage":"http://groovy-lang.org"}}}'
+            // end::streaming_json_builder_generator[]
+        '''
+    }
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/test/groovy/groovy/json/CharBufTest.groovy
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/test/groovy/groovy/json/CharBufTest.groovy b/subprojects/groovy-json/src/test/groovy/groovy/json/CharBufTest.groovy
index d19718e..8915f55 100644
--- a/subprojects/groovy-json/src/test/groovy/groovy/json/CharBufTest.groovy
+++ b/subprojects/groovy-json/src/test/groovy/groovy/json/CharBufTest.groovy
@@ -63,6 +63,17 @@ class CharBufTest extends GroovyTestCase {
         assert str == '" \\\\ "'
     }
 
+    void testDisableUnicodeEscaping() {
+        String str = CharBuf.create(0).addJsonEscapedString("�ric").toString()
+        assert str == '"\\u00c9ric"'
+
+        str = CharBuf.create(0).addJsonEscapedString("�ric", false).toString()
+        assert str == '"\\u00c9ric"'
+
+        str = CharBuf.create(0).addJsonEscapedString("�ric", true).toString()
+        assert str == '"�ric"'
+    }
+
     /**
      * https://issues.apache.org/jira/browse/GROOVY-6937
      * https://issues.apache.org/jira/browse/GROOVY-6852
@@ -84,4 +95,28 @@ class CharBufTest extends GroovyTestCase {
         result = new JsonBuilder(obj).toString()
         assert result == /["${'\\u20ac' * 20_000}"]/
     }
+
+    void testRemoveLastChar() {
+        CharBuf buffer
+
+        buffer = CharBuf.create(8).add('value1,')
+        buffer.removeLastChar()
+        assert buffer.toString() == 'value1'
+
+        buffer = CharBuf.create(4)
+        buffer.removeLastChar()
+        assert buffer.toString() == ''
+
+        buffer = CharBuf.create(8).add('[]')
+        buffer.removeLastChar((char)',')
+        assert buffer.toString() == '[]'
+
+        buffer = CharBuf.create(8).add('[val,')
+        buffer.removeLastChar((char)',')
+        assert buffer.toString() == '[val'
+
+        buffer = CharBuf.create(32).add('[one,two,three,four,')
+        buffer.removeLastChar((char)',')
+        assert buffer.toString() == '[one,two,three,four'
+    }
 }

http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/test/groovy/groovy/json/CustomJsonGeneratorTest.groovy
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/test/groovy/groovy/json/CustomJsonGeneratorTest.groovy b/subprojects/groovy-json/src/test/groovy/groovy/json/CustomJsonGeneratorTest.groovy
new file mode 100644
index 0000000..38a73fa
--- /dev/null
+++ b/subprojects/groovy-json/src/test/groovy/groovy/json/CustomJsonGeneratorTest.groovy
@@ -0,0 +1,89 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.json
+
+import groovy.json.JsonGenerator.Converter
+import groovy.json.JsonGenerator.Options
+import groovy.json.internal.CharBuf
+
+/**
+ * Tests extensibility of JsonGenerator and associated classes
+ */
+class CustomJsonGeneratorTest extends GroovyTestCase {
+
+    void testCustomGenerator() {
+        def generator = new CustomJsonOptions()
+                            .excludeNulls()
+                            .lowerCaseFieldNames()
+                            .addCustomConverter(new CustomJsonConverter())
+                            .build()
+
+        assert generator.toJson(['one', null, 'two', null]) == '["one","two"]'
+        assert generator.toJson(['Foo':'test1', 'BAR':'test2']) == '{"foo":"test1","bar":"test2"}'
+        assert generator.toJson(['foo': new CustomFoo()]) == '{"foo":"CustomFoo from CustomJsonConverter"}'
+    }
+
+    static class CustomJsonOptions extends Options {
+        boolean lowerCaseFieldNames
+        CustomJsonOptions lowerCaseFieldNames() {
+            lowerCaseFieldNames = true
+            return this
+        }
+        CustomJsonOptions addCustomConverter(Converter converter) {
+            converters.add(converter)
+            return this
+        }
+        @Override
+        CustomJsonGenerator build() {
+            return new CustomJsonGenerator(this)
+        }
+    }
+
+    static class CustomJsonGenerator extends DefaultJsonGenerator {
+        boolean lowerCaseFieldNames
+        CustomJsonGenerator(CustomJsonOptions opts) {
+            super(opts)
+            lowerCaseFieldNames = opts.lowerCaseFieldNames
+        }
+        @Override
+        protected void writeMapEntry(String key, Object value, CharBuf buffer) {
+            String newKey = (lowerCaseFieldNames) ? key.toLowerCase() : key
+            super.writeMapEntry(newKey, value, buffer)
+        }
+    }
+
+    static class CustomJsonConverter implements Converter {
+        @Override
+        boolean handles(Class<?> type) {
+            return CustomFoo.isAssignableFrom(type)
+        }
+
+        @Override
+        CharSequence convert(Object value) {
+            return convert(value, null)
+        }
+
+        @Override
+        CharSequence convert(Object value, String key) {
+            return '"CustomFoo from CustomJsonConverter"'
+        }
+    }
+
+    static class CustomFoo {}
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/test/groovy/groovy/json/DefaultJsonGeneratorTest.groovy
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/test/groovy/groovy/json/DefaultJsonGeneratorTest.groovy b/subprojects/groovy-json/src/test/groovy/groovy/json/DefaultJsonGeneratorTest.groovy
new file mode 100644
index 0000000..167ce31
--- /dev/null
+++ b/subprojects/groovy-json/src/test/groovy/groovy/json/DefaultJsonGeneratorTest.groovy
@@ -0,0 +1,283 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.json
+
+class DefaultJsonGeneratorTest extends GroovyTestCase {
+
+    void testExcludesNullValues() {
+        def generator = new JsonGenerator.Options()
+                .excludeNulls()
+                .build()
+
+        def json = generator.toJson(new JsonObject(name: 'test', properties: null))
+        assert json == '{"name":"test"}'
+
+        json = generator.toJson([field1: null, field2: "test"])
+        assert json == '{"field2":"test"}'
+
+        assert generator.toJson([null]) == '[]'
+        assert generator.toJson(['a','b','c','d', null]) == '["a","b","c","d"]'
+        assert generator.toJson(['a', null, null, null, null]) == '["a"]'
+        assert generator.toJson(['a', null, null, null, 'e']) == '["a","e"]'
+
+        def jsonArray = ["foo", null, "bar"]
+        def jsonExpected = '["foo","bar"]'
+        assert generator.toJson(jsonArray) == jsonExpected
+        assert generator.toJson(jsonArray as Object[]) == jsonExpected
+        assert generator.toJson(jsonArray.iterator()) == jsonExpected
+        assert generator.toJson((Iterable)jsonArray) == jsonExpected
+
+        assert generator.toJson((Boolean)null) == ''
+        assert generator.toJson((Number)null) == ''
+        assert generator.toJson((Character)null) == ''
+        assert generator.toJson((String)null) == ''
+        assert generator.toJson((Date)null) == ''
+        assert generator.toJson((Calendar)null) == ''
+        assert generator.toJson((UUID)null) == ''
+        assert generator.toJson((Closure)null) == ''
+        assert generator.toJson((Expando)null) == ''
+        assert generator.toJson((Object)null) == ''
+        assert generator.toJson((Map)null) == ''
+    }
+
+    void testCustomDateFormat() {
+        def generator = new JsonGenerator.Options()
+                .dateFormat('yyyy-MM')
+                .build()
+
+        Date aDate = Date.parse('yyyy-MM-dd', '2016-07-04')
+        assert generator.toJson(aDate) == '"2016-07"'
+
+        def jsonObject = new JsonObject(name: 'test', properties: [startDate: aDate])
+        def json = generator.toJson(jsonObject)
+        assert json.contains('{"startDate":"2016-07"}')
+
+        def jsonArray = ["foo", aDate, "bar"]
+        def jsonExpected = '["foo","2016-07","bar"]'
+        assert generator.toJson(jsonArray) == jsonExpected
+        assert generator.toJson(jsonArray as Object[]) == jsonExpected
+        assert generator.toJson(jsonArray.iterator()) == jsonExpected
+        assert generator.toJson((Iterable)jsonArray) == jsonExpected
+    }
+
+    void testDateFormatBadInput() {
+        shouldFail(NullPointerException) {
+            new JsonGenerator.Options().dateFormat(null)
+        }
+        shouldFail(IllegalArgumentException) {
+            new JsonGenerator.Options().dateFormat('abcde')
+        }
+        shouldFail(NullPointerException) {
+            new JsonGenerator.Options().timezone(null)
+        }
+    }
+
+    void testConverters() {
+        def generator = new JsonGenerator.Options()
+                .addConverter(JsonCyclicReference) { object, key ->
+            return '"JsonCyclicReference causes a stackoverflow"'
+        }
+        .addConverter(Date) { object ->
+            return '"4 score and 7 years ago"'
+        }
+        .addConverter(Calendar) { object ->
+            return '"22 days ago"'
+        }
+        .build()
+
+        assert generator.toJson(new Date()) == '"4 score and 7 years ago"'
+
+        def ref = new JsonBar('bar', new Date())
+        def json = generator.toJson(ref)
+        assert json.contains('"lastVisit":"4 score and 7 years ago"')
+        assert json.contains('"cycle":"JsonCyclicReference causes a stackoverflow"')
+
+        def jsonArray = ["foo", new JsonCyclicReference(), "bar", new Date()]
+        def jsonExpected = '["foo","JsonCyclicReference causes a stackoverflow","bar","4 score and 7 years ago"]'
+        assert generator.toJson(jsonArray) == jsonExpected
+        assert generator.toJson(jsonArray as Object[]) == jsonExpected
+        assert generator.toJson(jsonArray.iterator()) == jsonExpected
+        assert generator.toJson((Iterable)jsonArray) == jsonExpected
+
+        assert generator.toJson([timeline: Calendar.getInstance()]) == '{"timeline":"22 days ago"}'
+    }
+
+    void testConverterAddedLastTakesPrecedence() {
+        def options = new JsonGenerator.Options()
+        def c1 = { 'c1' }
+        def c2 = { 'c2' }
+        options.addConverter(URL, {})
+        options.addConverter(Date, c1)
+        options.addConverter(Calendar, {})
+        options.addConverter(Date, c2)
+        options.addConverter(java.sql.Date, {})
+
+        assert options.@converters.size() == 4
+        assert options.@converters[2].convert(null) == 'c2'
+        assert !options.@converters.find { it.convert(null) == 'c1' }
+    }
+
+    void testConvertersBadInput() {
+        shouldFail(NullPointerException) {
+            new JsonGenerator.Options().addConverter(null, null)
+        }
+        shouldFail(NullPointerException) {
+            new JsonGenerator.Options().addConverter(Date, null)
+        }
+        shouldFail(IllegalArgumentException) {
+            new JsonGenerator.Options().addConverter(Date, {-> 'no args closure'})
+        }
+        shouldFail(IllegalArgumentException) {
+            new JsonGenerator.Options().addConverter(Date, { UUID obj -> 'mis-matched types'})
+        }
+        shouldFail(IllegalArgumentException) {
+            new JsonGenerator.Options().addConverter(Date, { Date obj, UUID cs -> 'mis-matched types'})
+        }
+    }
+
+    void testExcludesFieldsByName() {
+        def generator = new JsonGenerator.Options()
+                .excludeFieldsByName('name')
+                .build()
+
+        def ref = new JsonObject(name: 'Jason', properties: ['foo': 'bar'])
+        def json = generator.toJson(ref)
+        assert json == '{"properties":{"foo":"bar"}}'
+
+        def jsonArray = ["foo", ["bar":"test","name":"Jane"], "baz"]
+        def jsonExpected = '["foo",{"bar":"test"},"baz"]'
+        assert generator.toJson(jsonArray) == jsonExpected
+        assert generator.toJson(jsonArray as Object[]) == jsonExpected
+        assert generator.toJson(jsonArray.iterator()) == jsonExpected
+        assert generator.toJson((Iterable)jsonArray) == jsonExpected
+
+        def excludeList = ['foo', 'bar', "${'zoo'}"]
+        generator = new JsonGenerator.Options()
+                .excludeFieldsByName(excludeList)
+                .build()
+
+        json = generator.toJson([foo: 'one', bar: 'two', baz: 'three', zoo: 'four'])
+        assert json == '{"baz":"three"}'
+    }
+
+    void testExcludeFieldsByNameBadInput() {
+        shouldFail(NullPointerException) {
+            new JsonGenerator.Options().excludeFieldsByName(null)
+        }
+    }
+
+    void testExcludeFieldsByNameShouldIgnoreNulls() {
+        def opts = new JsonGenerator.Options()
+                .excludeFieldsByName('foo', null, "${'bar'}")
+                .excludeFieldsByName([new StringBuilder('one'), null, 'two'])
+
+        assert opts.@excludedFieldNames.size() == 4
+        assert !opts.@excludedFieldNames.contains(null)
+    }
+
+    void testExcludesFieldsByType() {
+        def generator = new JsonGenerator.Options()
+                .excludeFieldsByType(Date)
+                .build()
+
+        def ref = [name: 'Jason', dob: new Date(), location: 'Los Angeles']
+        assert generator.toJson(ref) == '{"name":"Jason","location":"Los Angeles"}'
+
+        def jsonArray = ["foo", "bar", new Date()]
+        def jsonExpected = '["foo","bar"]'
+        assert generator.toJson(jsonArray) == jsonExpected
+        assert generator.toJson(jsonArray as Object[]) == jsonExpected
+        assert generator.toJson(jsonArray.iterator()) == jsonExpected
+        assert generator.toJson((Iterable)jsonArray) == jsonExpected
+
+        generator = new JsonGenerator.Options()
+                .excludeFieldsByType(Integer)
+                .excludeFieldsByType(Boolean)
+                .excludeFieldsByType(Character)
+                .excludeFieldsByType(Calendar)
+                .excludeFieldsByType(UUID)
+                .excludeFieldsByType(URL)
+                .excludeFieldsByType(Closure)
+                .excludeFieldsByType(Expando)
+                .excludeFieldsByType(TreeMap)
+                .excludeFieldsByType(Date)
+                .build()
+
+        assert generator.toJson(Integer.valueOf(7)) == ''
+        assert generator.toJson(Boolean.TRUE) == ''
+        assert generator.toJson((Character)'c') == ''
+        assert generator.toJson(Calendar.getInstance()) == ''
+        assert generator.toJson(UUID.randomUUID()) == ''
+        assert generator.toJson(new URL('http://groovy-lang.org')) == ''
+        assert generator.toJson({ url new URL('http://groovy-lang.org') }) == ''
+        assert generator.toJson(new Expando()) == ''
+        assert generator.toJson(new TreeMap()) == ''
+        assert generator.toJson(new java.sql.Date(new Date().getTime())) == ''
+
+        def excludeList = [URL, Date]
+        generator = new JsonGenerator.Options()
+                .excludeFieldsByType(excludeList)
+                .build()
+
+        def json = generator.toJson([foo: new Date(), bar: 'two', baz: new URL('http://groovy-lang.org')])
+        assert json == '{"bar":"two"}'
+    }
+
+    void testExcludeFieldsByTypeBadInput() {
+        shouldFail(NullPointerException) {
+            new JsonGenerator.Options().excludeFieldsByType(null)
+        }
+    }
+
+    void testExcludeFieldsByTypeShouldIgnoreNulls() {
+        def opts = new JsonGenerator.Options()
+                .excludeFieldsByType(Date, null, URL)
+                .excludeFieldsByType([Calendar, null, TreeMap])
+
+        assert opts.@excludedFieldTypes.size() == 4
+        assert !opts.@excludedFieldTypes.contains(null)
+    }
+
+    void testDisableUnicodeEscaping() {
+        def json = new JsonGenerator.Options()
+                .disableUnicodeEscaping()
+                .build()
+
+        String unicodeString = '\u039a\u03a1\u0395\u03a9\u03a0\u039f\u039b\u0395\u0399\u039f'
+        assert json.toJson([unicodeString]) == """["${unicodeString}"]"""
+
+        assert json.toJson(['K�Y':'VALUE']) == '{"K�Y":"VALUE"}'
+    }
+
+}
+
+class JsonBar {
+    String favoriteDrink
+    Date lastVisit
+    JsonCyclicReference cycle = new JsonCyclicReference()
+    JsonBar(String favoriteDrink, Date lastVisit) {
+        this.favoriteDrink = favoriteDrink
+        this.lastVisit = lastVisit
+    }
+}
+
+class JsonCyclicReference {
+    static final DEFAULT = new JsonCyclicReference()
+    JsonCyclicReference() { }
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/test/groovy/groovy/json/JsonBuilderTest.groovy
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/test/groovy/groovy/json/JsonBuilderTest.groovy b/subprojects/groovy-json/src/test/groovy/groovy/json/JsonBuilderTest.groovy
index ec643f0..79c88e3 100644
--- a/subprojects/groovy-json/src/test/groovy/groovy/json/JsonBuilderTest.groovy
+++ b/subprojects/groovy-json/src/test/groovy/groovy/json/JsonBuilderTest.groovy
@@ -402,4 +402,31 @@ class JsonBuilderTest extends GroovyTestCase {
         assert new JsonBuilder({'\1' 0}).toString() == '{"\\u0001":0}'
         assert new JsonBuilder({'\u0002' 0}).toString() == '{"\\u0002":0}'
     }
+
+    void testWithGenerator() {
+        def generator = new JsonGenerator.Options()
+                .excludeNulls()
+                .dateFormat('yyyyMM')
+                .excludeFieldsByName('secretKey', 'creditCardNumber')
+                .excludeFieldsByType(URL)
+                .addConverter(java.util.concurrent.atomic.AtomicBoolean) { ab -> ab.toString() }
+                .build()
+
+        def json = new JsonBuilder(generator)
+
+        json.payload {
+            id 'YT-1234'
+            location null
+            secretKey 'J79-A25'
+            creditCardNumber '123-444-789-2233'
+            site new URL('http://groovy-lang.org')
+            isActive new java.util.concurrent.atomic.AtomicBoolean(true)
+        }
+
+        assert json.toString() == '{"payload":{"id":"YT-1234","isActive":true}}'
+
+        json = new JsonBuilder(['foo', null, 'bar', new URL('http://groovy-lang.org')], generator)
+        assert json.toString() == '["foo","bar"]'
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/test/groovy/groovy/json/StreamingJsonBuilderTest.groovy
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/test/groovy/groovy/json/StreamingJsonBuilderTest.groovy b/subprojects/groovy-json/src/test/groovy/groovy/json/StreamingJsonBuilderTest.groovy
index 25ac0ae..a3cf4d8 100644
--- a/subprojects/groovy-json/src/test/groovy/groovy/json/StreamingJsonBuilderTest.groovy
+++ b/subprojects/groovy-json/src/test/groovy/groovy/json/StreamingJsonBuilderTest.groovy
@@ -496,4 +496,54 @@ class StreamingJsonBuilderTest extends GroovyTestCase {
             }
         }
     }
+
+    void testWithGenerator() {
+        def generator = new JsonGenerator.Options()
+                .excludeNulls()
+                .dateFormat('yyyyMM')
+                .excludeFieldsByName('secretKey', 'creditCardNumber')
+                .excludeFieldsByType(URL)
+                .addConverter(java.util.concurrent.atomic.AtomicBoolean) { ab -> ab.toString() }
+                .build()
+
+        new StringWriter().with { w ->
+            def builder = new StreamingJsonBuilder(w, generator)
+
+            builder.payload {
+                id 'YT-1234'
+                location null
+                secretKey 'J79-A25'
+                creditCardNumber '123-444-789-2233'
+                site new URL('http://groovy-lang.org')
+                isActive new java.util.concurrent.atomic.AtomicBoolean(true)
+            }
+
+            assert w.toString() == '{"payload":{"id":"YT-1234","isActive":true}}'
+        }
+    }
+
+    @CompileStatic
+    void testWithGeneratorCompileStatic() {
+        def generator = new JsonGenerator.Options()
+                .excludeNulls()
+                .dateFormat('yyyyMM')
+                .excludeFieldsByName('secretKey', 'creditCardNumber')
+                .excludeFieldsByType(URL)
+                .addConverter(java.util.concurrent.atomic.AtomicBoolean) { ab -> ab.toString() }
+                .build()
+
+        new StringWriter().with { w ->
+            def builder = new StreamingJsonBuilder(w, generator)
+            builder.call('payload') {
+                call 'id', 'YT-1234'
+                call 'location', (String)null
+                call 'secretKey', 'J79-A25'
+                call 'creditCardNumber', '123-444-789-2233'
+                call 'site', new URL('http://groovy-lang.org')
+                call 'isActive', new java.util.concurrent.atomic.AtomicBoolean(true)
+            }
+
+            assert w.toString() == '{"payload":{"id":"YT-1234","isActive":true}}'
+        }
+    }
 }


[2/2] groovy git commit: JsonGenerator - JSON serialization options (closes #371, closes #433)

Posted by jw...@apache.org.
JsonGenerator - JSON serialization options (closes #371, closes #433)

Fixes or partially addresses the following:

GROOVY-6699: ignore properties/fields during serialization
GROOVY-6854: serialize ISO-8601 dates
GROOVY-6975: deactivate unicode escaping
GROOVY-7682: JodaTime/JSR310 (using custom converter)
GROOVY-7780: exclude null values


Project: http://git-wip-us.apache.org/repos/asf/groovy/repo
Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/13202599
Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/13202599
Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/13202599

Branch: refs/heads/master
Commit: 132025997e110e284fe1ce5e4d19a75ae5418ae6
Parents: 8213bd0
Author: John Wagenleitner <jw...@apache.org>
Authored: Sun Oct 23 16:40:11 2016 -0700
Committer: John Wagenleitner <jw...@apache.org>
Committed: Sun Oct 23 16:40:11 2016 -0700

----------------------------------------------------------------------
 .../java/groovy/json/DefaultJsonGenerator.java  | 549 +++++++++++++++++++
 .../src/main/java/groovy/json/JsonBuilder.java  |  28 +-
 .../main/java/groovy/json/JsonGenerator.java    | 326 +++++++++++
 .../src/main/java/groovy/json/JsonOutput.java   | 372 +------------
 .../java/groovy/json/StreamingJsonBuilder.java  | 134 ++++-
 .../main/java/groovy/json/internal/CharBuf.java |  57 +-
 .../groovy-json/src/spec/doc/json-builder.adoc  |   9 +-
 .../src/spec/doc/json-userguide.adoc            |  28 +-
 .../src/spec/doc/streaming-jason-builder.adoc   |   9 +-
 .../src/spec/test/json/JsonBuilderTest.groovy   |  32 ++
 .../src/spec/test/json/JsonTest.groovy          |  68 +++
 .../test/json/StreamingJsonBuilderTest.groovy   |  35 +-
 .../test/groovy/groovy/json/CharBufTest.groovy  |  35 ++
 .../groovy/json/CustomJsonGeneratorTest.groovy  |  89 +++
 .../groovy/json/DefaultJsonGeneratorTest.groovy | 283 ++++++++++
 .../groovy/groovy/json/JsonBuilderTest.groovy   |  27 +
 .../groovy/json/StreamingJsonBuilderTest.groovy |  50 ++
 17 files changed, 1729 insertions(+), 402 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/main/java/groovy/json/DefaultJsonGenerator.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/groovy/json/DefaultJsonGenerator.java b/subprojects/groovy-json/src/main/java/groovy/json/DefaultJsonGenerator.java
new file mode 100644
index 0000000..1dbd0ad
--- /dev/null
+++ b/subprojects/groovy-json/src/main/java/groovy/json/DefaultJsonGenerator.java
@@ -0,0 +1,549 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.json;
+
+import groovy.json.internal.CharBuf;
+import groovy.json.internal.Chr;
+import groovy.lang.Closure;
+import groovy.util.Expando;
+import org.codehaus.groovy.runtime.DefaultGroovyMethods;
+
+import java.io.File;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.URL;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.UUID;
+
+import static groovy.json.JsonOutput.CLOSE_BRACE;
+import static groovy.json.JsonOutput.CLOSE_BRACKET;
+import static groovy.json.JsonOutput.COMMA;
+import static groovy.json.JsonOutput.EMPTY_LIST_CHARS;
+import static groovy.json.JsonOutput.EMPTY_MAP_CHARS;
+import static groovy.json.JsonOutput.EMPTY_STRING_CHARS;
+import static groovy.json.JsonOutput.OPEN_BRACE;
+import static groovy.json.JsonOutput.OPEN_BRACKET;
+
+/**
+ * A JsonGenerator that can be configured with various {@link JsonGenerator.Options}.
+ * If the default options are sufficient consider using the static {@code JsonOutput.toJson}
+ * methods.
+ *
+ * @see JsonGenerator.Options#build()
+ * @since 2.5
+ */
+public class DefaultJsonGenerator implements JsonGenerator {
+
+    protected final boolean excludeNulls;
+    protected final boolean disableUnicodeEscaping;
+    protected final String dateFormat;
+    protected final Locale dateLocale;
+    protected final TimeZone timezone;
+    protected final Set<Converter> converters = new LinkedHashSet<Converter>();
+    protected final Set<String> excludedFieldNames = new HashSet<String>();
+    protected final Set<Class<?>> excludedFieldTypes = new HashSet<Class<?>>();
+
+    protected DefaultJsonGenerator(Options options) {
+        excludeNulls = options.excludeNulls;
+        disableUnicodeEscaping = options.disableUnicodeEscaping;
+        dateFormat = options.dateFormat;
+        dateLocale = options.dateLocale;
+        timezone = options.timezone;
+        if (!options.converters.isEmpty()) {
+            converters.addAll(options.converters);
+        }
+        if (!options.excludedFieldNames.isEmpty()) {
+            excludedFieldNames.addAll(options.excludedFieldNames);
+        }
+        if (!options.excludedFieldTypes.isEmpty()) {
+            excludedFieldTypes.addAll(options.excludedFieldTypes);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toJson(Object object) {
+        CharBuf buffer = CharBuf.create(255);
+        writeObject(object, buffer);
+        return buffer.toString();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isExcludingFieldsNamed(String name) {
+        return excludedFieldNames.contains(name);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isExcludingValues(Object value) {
+        if (value == null) {
+            return excludeNulls;
+        } else {
+            return shouldExcludeType(value.getClass());
+        }
+    }
+
+    /**
+     * Serializes Number value and writes it into specified buffer.
+     */
+    protected void writeNumber(Class<?> numberClass, Number value, CharBuf buffer) {
+        if (numberClass == Integer.class) {
+            buffer.addInt((Integer) value);
+        } else if (numberClass == Long.class) {
+            buffer.addLong((Long) value);
+        } else if (numberClass == BigInteger.class) {
+            buffer.addBigInteger((BigInteger) value);
+        } else if (numberClass == BigDecimal.class) {
+            buffer.addBigDecimal((BigDecimal) value);
+        } else if (numberClass == Double.class) {
+            Double doubleValue = (Double) value;
+            if (doubleValue.isInfinite()) {
+                throw new JsonException("Number " + value + " can't be serialized as JSON: infinite are not allowed in JSON.");
+            }
+            if (doubleValue.isNaN()) {
+                throw new JsonException("Number " + value + " can't be serialized as JSON: NaN are not allowed in JSON.");
+            }
+
+            buffer.addDouble(doubleValue);
+        } else if (numberClass == Float.class) {
+            Float floatValue = (Float) value;
+            if (floatValue.isInfinite()) {
+                throw new JsonException("Number " + value + " can't be serialized as JSON: infinite are not allowed in JSON.");
+            }
+            if (floatValue.isNaN()) {
+                throw new JsonException("Number " + value + " can't be serialized as JSON: NaN are not allowed in JSON.");
+            }
+
+            buffer.addFloat(floatValue);
+        } else if (numberClass == Byte.class) {
+            buffer.addByte((Byte) value);
+        } else if (numberClass == Short.class) {
+            buffer.addShort((Short) value);
+        } else { // Handle other Number implementations
+            buffer.addString(value.toString());
+        }
+    }
+
+    protected void writeObject(Object object, CharBuf buffer) {
+        writeObject(null, object, buffer);
+    }
+
+    /**
+     * Serializes object and writes it into specified buffer.
+     */
+    protected void writeObject(String key, Object object, CharBuf buffer) {
+
+        if (isExcludingValues(object)) {
+            return;
+        }
+
+        if (object == null) {
+            buffer.addNull();
+            return;
+        }
+
+        Class<?> objectClass = object.getClass();
+
+        Converter converter = findConverter(objectClass);
+        if (converter != null) {
+            writeRaw(converter.convert(object, key), buffer);
+            return;
+        }
+
+        if (CharSequence.class.isAssignableFrom(objectClass)) { // Handle String, StringBuilder, GString and other CharSequence implementations
+            writeCharSequence((CharSequence) object, buffer);
+        } else if (objectClass == Boolean.class) {
+            buffer.addBoolean((Boolean) object);
+        } else if (Number.class.isAssignableFrom(objectClass)) {
+            writeNumber(objectClass, (Number) object, buffer);
+        } else if (Date.class.isAssignableFrom(objectClass)) {
+            writeDate((Date) object, buffer);
+        } else if (Calendar.class.isAssignableFrom(objectClass)) {
+            writeDate(((Calendar) object).getTime(), buffer);
+        } else if (Map.class.isAssignableFrom(objectClass)) {
+            writeMap((Map) object, buffer);
+        } else if (Iterable.class.isAssignableFrom(objectClass)) {
+            writeIterator(((Iterable<?>) object).iterator(), buffer);
+        } else if (Iterator.class.isAssignableFrom(objectClass)) {
+            writeIterator((Iterator) object, buffer);
+        } else if (objectClass == Character.class) {
+            buffer.addJsonEscapedString(Chr.array((Character) object), disableUnicodeEscaping);
+        } else if (objectClass == URL.class) {
+            buffer.addJsonEscapedString(object.toString(), disableUnicodeEscaping);
+        } else if (objectClass == UUID.class) {
+            buffer.addQuoted(object.toString());
+        } else if (objectClass == JsonOutput.JsonUnescaped.class) {
+            buffer.add(object.toString());
+        } else if (Closure.class.isAssignableFrom(objectClass)) {
+            writeMap(JsonDelegate.cloneDelegateAndGetContent((Closure<?>) object), buffer);
+        } else if (Expando.class.isAssignableFrom(objectClass)) {
+            writeMap(((Expando) object).getProperties(), buffer);
+        } else if (Enumeration.class.isAssignableFrom(objectClass)) {
+            List<?> list = Collections.list((Enumeration<?>) object);
+            writeIterator(list.iterator(), buffer);
+        } else if (objectClass.isArray()) {
+            writeArray(objectClass, object, buffer);
+        } else if (Enum.class.isAssignableFrom(objectClass)) {
+            buffer.addQuoted(((Enum<?>) object).name());
+        } else if (File.class.isAssignableFrom(objectClass)) {
+            Map<?, ?> properties = getObjectProperties(object);
+            //Clean up all recursive references to File objects
+            Iterator<? extends Map.Entry<?, ?>> iterator = properties.entrySet().iterator();
+            while(iterator.hasNext()) {
+                Map.Entry<?,?> entry = iterator.next();
+                if(entry.getValue() instanceof File) {
+                    iterator.remove();
+                }
+            }
+            writeMap(properties, buffer);
+        } else {
+            Map<?, ?> properties = getObjectProperties(object);
+            writeMap(properties, buffer);
+        }
+    }
+
+    protected Map<?, ?> getObjectProperties(Object object) {
+        Map<?, ?> properties = DefaultGroovyMethods.getProperties(object);
+        properties.remove("class");
+        properties.remove("declaringClass");
+        properties.remove("metaClass");
+        return properties;
+    }
+
+    /**
+     * Serializes any char sequence and writes it into specified buffer.
+     */
+    protected void writeCharSequence(CharSequence seq, CharBuf buffer) {
+        if (seq.length() > 0) {
+            buffer.addJsonEscapedString(seq.toString(), disableUnicodeEscaping);
+        } else {
+            buffer.addChars(EMPTY_STRING_CHARS);
+        }
+    }
+
+    /**
+     * Serializes any char sequence and writes it into specified buffer
+     * without performing any manipulation of the given text.
+     */
+    protected void writeRaw(CharSequence seq, CharBuf buffer) {
+        if (seq != null) {
+            buffer.add(seq.toString());
+        }
+    }
+
+    /**
+     * Serializes date and writes it into specified buffer.
+     */
+    protected void writeDate(Date date, CharBuf buffer) {
+        SimpleDateFormat formatter = new SimpleDateFormat(dateFormat, dateLocale);
+        formatter.setTimeZone(timezone);
+        buffer.addQuoted(formatter.format(date));
+    }
+
+    /**
+     * Serializes array and writes it into specified buffer.
+     */
+    protected void writeArray(Class<?> arrayClass, Object array, CharBuf buffer) {
+        if (Object[].class.isAssignableFrom(arrayClass)) {
+            Object[] objArray = (Object[]) array;
+            writeIterator(Arrays.asList(objArray).iterator(), buffer);
+            return;
+        }
+        buffer.addChar(OPEN_BRACKET);
+        if (int[].class.isAssignableFrom(arrayClass)) {
+            int[] intArray = (int[]) array;
+            if (intArray.length > 0) {
+                buffer.addInt(intArray[0]);
+                for (int i = 1; i < intArray.length; i++) {
+                    buffer.addChar(COMMA).addInt(intArray[i]);
+                }
+            }
+        } else if (long[].class.isAssignableFrom(arrayClass)) {
+            long[] longArray = (long[]) array;
+            if (longArray.length > 0) {
+                buffer.addLong(longArray[0]);
+                for (int i = 1; i < longArray.length; i++) {
+                    buffer.addChar(COMMA).addLong(longArray[i]);
+                }
+            }
+        } else if (boolean[].class.isAssignableFrom(arrayClass)) {
+            boolean[] booleanArray = (boolean[]) array;
+            if (booleanArray.length > 0) {
+                buffer.addBoolean(booleanArray[0]);
+                for (int i = 1; i < booleanArray.length; i++) {
+                    buffer.addChar(COMMA).addBoolean(booleanArray[i]);
+                }
+            }
+        } else if (char[].class.isAssignableFrom(arrayClass)) {
+            char[] charArray = (char[]) array;
+            if (charArray.length > 0) {
+                buffer.addJsonEscapedString(Chr.array(charArray[0]), disableUnicodeEscaping);
+                for (int i = 1; i < charArray.length; i++) {
+                    buffer.addChar(COMMA).addJsonEscapedString(Chr.array(charArray[i]), disableUnicodeEscaping);
+                }
+            }
+        } else if (double[].class.isAssignableFrom(arrayClass)) {
+            double[] doubleArray = (double[]) array;
+            if (doubleArray.length > 0) {
+                buffer.addDouble(doubleArray[0]);
+                for (int i = 1; i < doubleArray.length; i++) {
+                    buffer.addChar(COMMA).addDouble(doubleArray[i]);
+                }
+            }
+        } else if (float[].class.isAssignableFrom(arrayClass)) {
+            float[] floatArray = (float[]) array;
+            if (floatArray.length > 0) {
+                buffer.addFloat(floatArray[0]);
+                for (int i = 1; i < floatArray.length; i++) {
+                    buffer.addChar(COMMA).addFloat(floatArray[i]);
+                }
+            }
+        } else if (byte[].class.isAssignableFrom(arrayClass)) {
+            byte[] byteArray = (byte[]) array;
+            if (byteArray.length > 0) {
+                buffer.addByte(byteArray[0]);
+                for (int i = 1; i < byteArray.length; i++) {
+                    buffer.addChar(COMMA).addByte(byteArray[i]);
+                }
+            }
+        } else if (short[].class.isAssignableFrom(arrayClass)) {
+            short[] shortArray = (short[]) array;
+            if (shortArray.length > 0) {
+                buffer.addShort(shortArray[0]);
+                for (int i = 1; i < shortArray.length; i++) {
+                    buffer.addChar(COMMA).addShort(shortArray[i]);
+                }
+            }
+        }
+        buffer.addChar(CLOSE_BRACKET);
+    }
+
+    /**
+     * Serializes map and writes it into specified buffer.
+     */
+    protected void writeMap(Map<?, ?> map, CharBuf buffer) {
+        if (map.isEmpty()) {
+            buffer.addChars(EMPTY_MAP_CHARS);
+            return;
+        }
+        buffer.addChar(OPEN_BRACE);
+        for (Map.Entry<?, ?> entry : map.entrySet()) {
+            if (entry.getKey() == null) {
+                throw new IllegalArgumentException("Maps with null keys can\'t be converted to JSON");
+            }
+            String key = entry.getKey().toString();
+            Object value = entry.getValue();
+            if (isExcludingValues(value) || isExcludingFieldsNamed(key)) {
+                continue;
+            }
+            writeMapEntry(key, value, buffer);
+            buffer.addChar(COMMA);
+        }
+        buffer.removeLastChar(COMMA); // dangling comma
+        buffer.addChar(CLOSE_BRACE);
+    }
+
+    /**
+     * Serializes a map entry and writes it into specified buffer.
+     */
+    protected void writeMapEntry(String key, Object value, CharBuf buffer) {
+        buffer.addJsonFieldName(key, disableUnicodeEscaping);
+        writeObject(key, value, buffer);
+    }
+
+    /**
+     * Serializes iterator and writes it into specified buffer.
+     */
+    protected void writeIterator(Iterator<?> iterator, CharBuf buffer) {
+        if (!iterator.hasNext()) {
+            buffer.addChars(EMPTY_LIST_CHARS);
+            return;
+        }
+        buffer.addChar(OPEN_BRACKET);
+        while (iterator.hasNext()) {
+            Object it = iterator.next();
+            if (!isExcludingValues(it)) {
+                writeObject(it, buffer);
+                buffer.addChar(COMMA);
+            }
+        }
+        buffer.removeLastChar(COMMA); // dangling comma
+        buffer.addChar(CLOSE_BRACKET);
+    }
+
+    /**
+     * Finds a converter that can handle the given type.  The first converter
+     * that reports it can handle the type is returned, based on the order in
+     * which the converters were specified.  A {@code null} value will be returned
+     * if no suitable converter can be found for the given type.
+     *
+     * @param type that this converter can handle
+     * @return first converter that can handle the given type; else {@code null}
+     *         if no compatible converters are found for the given type.
+     */
+    protected Converter findConverter(Class<?> type) {
+        for (Converter c : converters) {
+            if (c.handles(type)) {
+                return c;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Indicates whether the given type should be excluded from the generated output.
+     *
+     * @param type the type to check
+     * @return {@code true} if the given type should not be output, else {@code false}
+     */
+    protected boolean shouldExcludeType(Class<?> type) {
+        for (Class<?> t : excludedFieldTypes) {
+            if (t.isAssignableFrom(type)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * A converter that handles converting a given type to a JSON value
+     * using a closure.
+     *
+     * @since 2.5
+     */
+    protected static class ClosureConverter implements Converter {
+
+        protected final Class<?> type;
+        protected final Closure<? extends CharSequence> closure;
+        protected final int paramCount;
+
+        protected ClosureConverter(Class<?> type, Closure<? extends CharSequence> closure) {
+            if (type == null) {
+                throw new NullPointerException("Type parameter must not be null");
+            }
+            if (closure == null) {
+                throw new NullPointerException("Closure parameter must not be null");
+            }
+
+            int paramCount = closure.getMaximumNumberOfParameters();
+            if (paramCount < 1) {
+                throw new IllegalArgumentException("Closure must accept at least one parameter");
+            }
+            Class<?> param1 = closure.getParameterTypes()[0];
+            if (!param1.isAssignableFrom(type)) {
+                throw new IllegalArgumentException("Expected first parameter to be of type: " + type.toString());
+            }
+            if (paramCount > 1) {
+                Class<?> param2 = closure.getParameterTypes()[1];
+                if (!param2.isAssignableFrom(String.class)) {
+                    throw new IllegalArgumentException("Expected second parameter to be of type: " + String.class.toString());
+                }
+            }
+            this.type = type;
+            this.closure = closure;
+            this.paramCount = paramCount;
+        }
+
+        /**
+         * Returns {@code true} if this converter can handle conversions
+         * of the given type.
+         *
+         * @param type the type of the object to convert
+         * @return true if this converter can successfully convert values of
+         *      the given type to a JSON value
+         */
+        public boolean handles(Class<?> type) {
+            return this.type.isAssignableFrom(type);
+        }
+
+        /**
+         * Converts a given value to a JSON value.
+         *
+         * @param value the object to convert
+         * @return a JSON value representing the value
+         */
+        public CharSequence convert(Object value) {
+            return convert(value, null);
+        }
+
+        /**
+         * Converts a given value to a JSON value.
+         *
+         * @param value the object to convert
+         * @param key the key name for the value, may be {@code null}
+         * @return a JSON value representing the value
+         */
+        public CharSequence convert(Object value, String key) {
+            return (paramCount == 1) ?
+                    closure.call(value) :
+                    closure.call(value, key);
+        }
+
+        /**
+         * Any two Converter instances registered for the same type are considered
+         * to be equal.  This comparison makes managing instances in a Set easier;
+         * since there is no chaining of Converters it makes sense to only allow
+         * one per type.
+         *
+         * @param o the object with which to compare.
+         * @return {@code true} if this object contains the same class; {@code false} otherwise.
+         */
+        @Override
+        public boolean equals(Object o) {
+            if (o == this) {
+                return true;
+            }
+            if (!(o instanceof ClosureConverter)) {
+                return false;
+            }
+            return this.type == ((ClosureConverter)o).type;
+        }
+
+        @Override
+        public int hashCode() {
+            return this.type.hashCode();
+        }
+
+        @Override
+        public String toString() {
+            return super.toString() + "<" + this.type.toString() + ">";
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/main/java/groovy/json/JsonBuilder.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/groovy/json/JsonBuilder.java b/subprojects/groovy-json/src/main/java/groovy/json/JsonBuilder.java
index 0a30b7d..abaac0b 100644
--- a/subprojects/groovy-json/src/main/java/groovy/json/JsonBuilder.java
+++ b/subprojects/groovy-json/src/main/java/groovy/json/JsonBuilder.java
@@ -65,12 +65,24 @@ import java.util.*;
  */
 public class JsonBuilder extends GroovyObjectSupport implements Writable {
 
+    private final JsonGenerator generator;
     private Object content;
 
     /**
      * Instantiates a JSON builder.
      */
     public JsonBuilder() {
+        this.generator = JsonOutput.DEFAULT_GENERATOR;
+    }
+
+    /**
+     * Instantiates a JSON builder with a configured generator.
+     *
+     * @param generator used to generate the output
+     * @since 2.5
+     */
+    public JsonBuilder(JsonGenerator generator) {
+        this.generator = generator;
     }
 
     /**
@@ -80,6 +92,20 @@ public class JsonBuilder extends GroovyObjectSupport implements Writable {
      */
     public JsonBuilder(Object content) {
         this.content = content;
+        this.generator = JsonOutput.DEFAULT_GENERATOR;
+    }
+
+    /**
+     * Instantiates a JSON builder with some existing data structure
+     * and a configured generator.
+     *
+     * @param content a pre-existing data structure
+     * @param generator used to generate the output
+     * @since 2.5
+     */
+    public JsonBuilder(Object content, JsonGenerator generator) {
+        this.content = content;
+        this.generator = generator;
     }
 
     public Object getContent() {
@@ -344,7 +370,7 @@ public class JsonBuilder extends GroovyObjectSupport implements Writable {
      * @return a JSON output
      */
     public String toString() {
-        return JsonOutput.toJson(content);
+        return generator.toJson(content);
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/main/java/groovy/json/JsonGenerator.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/groovy/json/JsonGenerator.java b/subprojects/groovy-json/src/main/java/groovy/json/JsonGenerator.java
new file mode 100644
index 0000000..91b0e06
--- /dev/null
+++ b/subprojects/groovy-json/src/main/java/groovy/json/JsonGenerator.java
@@ -0,0 +1,326 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.json;
+
+import groovy.lang.Closure;
+import groovy.transform.stc.ClosureParams;
+import groovy.transform.stc.FromString;
+
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.TimeZone;
+
+/**
+ * Generates JSON from objects.
+ *
+ * The {@link Options} builder can be used to configure an instance of a JsonGenerator.
+ *
+ * @see Options#build()
+ * @since 2.5
+ */
+public interface JsonGenerator {
+
+    /**
+     * Converts an object to its JSON representation.
+     *
+     * @param object to convert to JSON
+     * @return JSON
+     */
+    String toJson(Object object);
+
+    /**
+     * Indicates whether this JsonGenerator is configured to exclude fields by
+     * the given name.
+     *
+     * @param name of the field
+     * @return true if that field is being excluded, else false
+     */
+    boolean isExcludingFieldsNamed(String name);
+
+    /**
+     * Indicates whether this JsonGenerator is configured to exclude values
+     * of the given object (may be {@code null}).
+     *
+     * @param value an instance of an object
+     * @return true if values like this are being excluded, else false
+     */
+    boolean isExcludingValues(Object value);
+
+    /**
+     * Handles converting a given type to a JSON value.
+     *
+     * @since 2.5
+     */
+    interface Converter {
+
+        /**
+         * Returns {@code true} if this converter can handle conversions
+         * of the given type.
+         *
+         * @param type the type of the object to convert
+         * @return {@code true} if this converter can successfully convert values of
+         *      the given type to a JSON value, else {@code false}
+         */
+        boolean handles(Class<?> type);
+
+        /**
+         * Converts a given object to a JSON value.
+         *
+         * @param value the object to convert
+         * @return a JSON value representing the object
+         */
+        CharSequence convert(Object value);
+
+        /**
+         * Converts a given object to a JSON value.
+         *
+         * @param value the object to convert
+         * @param key the key name for the value, may be {@code null}
+         * @return a JSON value representing the object
+         */
+        CharSequence convert(Object value, String key);
+
+    }
+
+    /**
+     * A builder used to construct a {@link JsonGenerator} instance that allows
+     * control over the serialized JSON output.  If you do not need to customize the
+     * output it is recommended to use the static {@code JsonOutput.toJson} methods.
+     *
+     * <p>
+     * Example:
+     * <pre><code class="groovyTestCase">
+     *     def generator = new groovy.json.JsonGenerator.Options()
+     *                         .excludeNulls()
+     *                         .dateFormat('yyyy')
+     *                         .excludeFieldsByName('bar', 'baz')
+     *                         .excludeFieldsByType(java.sql.Date)
+     *                         .build()
+     *
+     *     def input = [foo: null, lastUpdated: Date.parse('yyyy-MM-dd', '2014-10-24'),
+     *                   bar: 'foo', baz: 'foo', systemDate: new java.sql.Date(new Date().getTime())]
+     *
+     *     assert generator.toJson(input) == '{"lastUpdated":"2014"}'
+     * </code></pre>
+     *
+     * @since 2.5
+     */
+    class Options {
+
+        protected static final String JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
+        protected static final Locale JSON_DATE_FORMAT_LOCALE = Locale.US;
+        protected static final String DEFAULT_TIMEZONE = "GMT";
+
+        protected boolean excludeNulls;
+        protected boolean disableUnicodeEscaping;
+        protected String dateFormat = JSON_DATE_FORMAT;
+        protected Locale dateLocale = JSON_DATE_FORMAT_LOCALE;
+        protected TimeZone timezone = TimeZone.getTimeZone(DEFAULT_TIMEZONE);
+        protected final Set<Converter> converters = new LinkedHashSet<Converter>();
+        protected final Set<String> excludedFieldNames = new HashSet<String>();
+        protected final Set<Class<?>> excludedFieldTypes = new HashSet<Class<?>>();
+
+        public Options() {}
+
+        /**
+         * Do not serialize {@code null} values.
+         *
+         * @return a reference to this {@code Options} instance
+         */
+        public Options excludeNulls() {
+            excludeNulls = true;
+            return this;
+        }
+
+        /**
+         * Disables the escaping of Unicode characters in JSON String values.
+         *
+         * @return a reference to this {@code Options} instance
+         */
+        public Options disableUnicodeEscaping() {
+            disableUnicodeEscaping = true;
+            return this;
+        }
+
+        /**
+         * Sets the date format that will be used to serialize {@code Date} objects.
+         * This must be a valid pattern for {@link java.text.SimpleDateFormat} and the
+         * date formatter will be constructed with the default locale of {@link Locale#US}.
+         *
+         * @param format date format pattern used to serialize dates
+         * @return a reference to this {@code Options} instance
+         * @exception NullPointerException if the given pattern is null
+         * @exception IllegalArgumentException if the given pattern is invalid
+         */
+        public Options dateFormat(String format) {
+            return dateFormat(format, JSON_DATE_FORMAT_LOCALE);
+        }
+
+        /**
+         * Sets the date format that will be used to serialize {@code Date} objects.
+         * This must be a valid pattern for {@link java.text.SimpleDateFormat}.
+         *
+         * @param format date format pattern used to serialize dates
+         * @param locale the locale whose date format symbols will be used
+         * @return a reference to this {@code Options} instance
+         * @exception IllegalArgumentException if the given pattern is invalid
+         */
+        public Options dateFormat(String format, Locale locale) {
+            // validate date format pattern
+            new SimpleDateFormat(format, locale);
+            dateFormat = format;
+            dateLocale = locale;
+            return this;
+        }
+
+        /**
+         * Sets the time zone that will be used to serialize dates.
+         *
+         * @param timezone used to serialize dates
+         * @return a reference to this {@code Options} instance
+         * @exception NullPointerException if the given timezone is null
+         */
+        public Options timezone(String timezone) {
+            this.timezone = TimeZone.getTimeZone(timezone);
+            return this;
+        }
+
+        /**
+         * Registers a closure that will be called when the specified type or subtype
+         * is serialized.
+         *
+         * <p>The closure must accept either 1 or 2 parameters.  The first parameter
+         * is required and will be instance of the {@code type} for which the closure
+         * is registered.  The second optional parameter should be of type {@code String}
+         * and, if available, will be passed the name of the key associated with this
+         * value if serializing a JSON Object.  This parameter will be {@code null} when
+         * serializing a JSON Array or when there is no way to determine the name of the key.
+         *
+         * <p>The return value from the closure must be a valid JSON value. The result
+         * of the closure will be written to the internal buffer directly and no quoting,
+         * escaping or other manipulation will be done to the resulting output.
+         *
+         * <p>
+         * Example:
+         * <pre><code class="groovyTestCase">
+         *     def generator = new groovy.json.JsonGenerator.Options()
+         *                         .addConverter(URL) { URL u ->
+         *                             "\"${u.getHost()}\""
+         *                         }
+         *                         .build()
+         *
+         *     def input = [domain: new URL('http://groovy-lang.org/json.html#_parser_variants')]
+         *
+         *     assert generator.toJson(input) == '{"domain":"groovy-lang.org"}'
+         * </code></pre>
+         *
+         * <p>If two or more closures are registered for the exact same type the last
+         * closure based on the order they were specified will be used.  When serializing an
+         * object its type is compared to the list of registered types in the order the were
+         * given and the closure for the first suitable type will be called.  Therefore, it is
+         * important to register more specific types first.
+         *
+         * @param type the type to convert
+         * @param closure called when the registered type or any type assignable to the given
+         *                type is encountered
+         * @param <T> the type this converter is registered to handle
+         * @return a reference to this {@code Options} instance
+         * @exception NullPointerException if the given type or closure is null
+         * @exception IllegalArgumentException if the given closure does not accept
+         *                  a parameter of the given type
+         */
+        public <T> Options addConverter(Class<T> type,
+                                        @ClosureParams(value=FromString.class, options={"T","T,String"})
+                                        Closure<? extends CharSequence> closure)
+        {
+            Converter converter = new DefaultJsonGenerator.ClosureConverter(type, closure);
+            if (converters.contains(converter)) {
+                converters.remove(converter);
+            }
+            converters.add(converter);
+            return this;
+        }
+
+        /**
+         * Excludes from the output any fields that match the specified names.
+         *
+         * @param fieldNames name of the field to exclude from the output
+         * @return a reference to this {@code Options} instance
+         */
+        public Options excludeFieldsByName(CharSequence... fieldNames) {
+            return excludeFieldsByName(Arrays.asList(fieldNames));
+        }
+
+        /**
+         * Excludes from the output any fields that match the specified names.
+         *
+         * @param fieldNames collection of names to exclude from the output
+         * @return a reference to this {@code Options} instance
+         */
+        public Options excludeFieldsByName(Iterable<? extends CharSequence> fieldNames) {
+            for (CharSequence cs : fieldNames) {
+                if (cs != null) {
+                    excludedFieldNames.add(cs.toString());
+                }
+            }
+            return this;
+        }
+
+        /**
+         * Excludes from the output any fields whose type is the same or is
+         * assignable to any of the given types.
+         *
+         * @param types excluded from the output
+         * @return a reference to this {@code Options} instance
+         */
+        public Options excludeFieldsByType(Class<?>... types) {
+            return excludeFieldsByType(Arrays.asList(types));
+        }
+
+        /**
+         * Excludes from the output any fields whose type is the same or is
+         * assignable to any of the given types.
+         *
+         * @param types collection of types to exclude from the output
+         * @return a reference to this {@code Options} instance
+         */
+        public Options excludeFieldsByType(Iterable<Class<?>> types) {
+            for (Class<?> c : types) {
+                if (c != null) {
+                    excludedFieldTypes.add(c);
+                }
+            }
+            return this;
+        }
+
+        /**
+         * Creates a {@link JsonGenerator} that is based on the current options.
+         *
+         * @return a fully configured {@link JsonGenerator}
+         */
+        public JsonGenerator build() {
+            return new DefaultJsonGenerator(this);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/main/java/groovy/json/JsonOutput.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/groovy/json/JsonOutput.java b/subprojects/groovy-json/src/main/java/groovy/json/JsonOutput.java
index 322e9f1..aa95b07 100644
--- a/subprojects/groovy-json/src/main/java/groovy/json/JsonOutput.java
+++ b/subprojects/groovy-json/src/main/java/groovy/json/JsonOutput.java
@@ -22,19 +22,17 @@ import groovy.json.internal.CharBuf;
 import groovy.json.internal.Chr;
 import groovy.lang.Closure;
 import groovy.util.Expando;
-import org.codehaus.groovy.runtime.DefaultGroovyMethods;
 
-import java.io.File;
 import java.io.StringReader;
-import java.math.BigDecimal;
-import java.math.BigInteger;
 import java.net.URL;
-import java.text.SimpleDateFormat;
 import java.util.*;
 
 /**
  * Class responsible for the actual String serialization of the possible values of a JSON structure.
  * This class can also be used as a category, so as to add <code>toJson()</code> methods to various types.
+ * <p>
+ * This class does not provide the ability to customize the resulting output.  A {@link JsonGenerator}
+ * can be used if the ability to alter the resulting output is required.
  *
  * @author Guillaume Laforge
  * @author Roshan Dawrani
@@ -42,6 +40,7 @@ import java.util.*;
  * @author Rick Hightower
  * @author Graeme Rocher
  *
+ * @see JsonGenerator
  * @since 1.8.0
  */
 public class JsonOutput {
@@ -56,20 +55,18 @@ public class JsonOutput {
     static final char NEW_LINE = '\n';
     static final char QUOTE = '"';
 
-    private static final char[] EMPTY_STRING_CHARS = Chr.array(QUOTE, QUOTE);
+    static final char[] EMPTY_STRING_CHARS = Chr.array(QUOTE, QUOTE);
+    static final char[] EMPTY_MAP_CHARS = {OPEN_BRACE, CLOSE_BRACE};
+    static final char[] EMPTY_LIST_CHARS = {OPEN_BRACKET, CLOSE_BRACKET};
 
-    private static final String NULL_VALUE = "null";
-    private static final String JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
-    private static final String DEFAULT_TIMEZONE = "GMT";
+    /* package-private for use in builders */
+    static final JsonGenerator DEFAULT_GENERATOR = new DefaultJsonGenerator(new JsonGenerator.Options());
 
     /**
      * @return "true" or "false" for a boolean value
      */
     public static String toJson(Boolean bool) {
-        CharBuf buffer = CharBuf.create(4);
-        writeObject(bool, buffer); // checking null inside
-
-        return buffer.toString();
+        return DEFAULT_GENERATOR.toJson(bool);
     }
 
     /**
@@ -77,39 +74,21 @@ public class JsonOutput {
      * @throws JsonException if the number is infinite or not a number.
      */
     public static String toJson(Number n) {
-        if (n == null) {
-            return NULL_VALUE;
-        }
-
-        CharBuf buffer = CharBuf.create(3);
-        Class<?> numberClass = n.getClass();
-        writeNumber(numberClass, n, buffer);
-
-        return buffer.toString();
+        return DEFAULT_GENERATOR.toJson(n);
     }
 
     /**
      * @return a JSON string representation of the character
      */
     public static String toJson(Character c) {
-        CharBuf buffer = CharBuf.create(3);
-        writeObject(c, buffer); // checking null inside
-
-        return buffer.toString();
+        return DEFAULT_GENERATOR.toJson(c);
     }
 
     /**
      * @return a properly encoded string with escape sequences
      */
     public static String toJson(String s) {
-        if (s == null) {
-            return NULL_VALUE;
-        }
-
-        CharBuf buffer = CharBuf.create(s.length() + 2);
-        writeCharSequence(s, buffer);
-
-        return buffer.toString();
+        return DEFAULT_GENERATOR.toJson(s);
     }
 
     /**
@@ -119,14 +98,7 @@ public class JsonOutput {
      * @return a formatted date in the form of a string
      */
     public static String toJson(Date date) {
-        if (date == null) {
-            return NULL_VALUE;
-        }
-
-        CharBuf buffer = CharBuf.create(26);
-        writeDate(date, buffer);
-
-        return buffer.toString();
+        return DEFAULT_GENERATOR.toJson(date);
     }
 
     /**
@@ -136,62 +108,35 @@ public class JsonOutput {
      * @return a formatted date in the form of a string
      */
     public static String toJson(Calendar cal) {
-        if (cal == null) {
-            return NULL_VALUE;
-        }
-
-        CharBuf buffer = CharBuf.create(26);
-        writeDate(cal.getTime(), buffer);
-
-        return buffer.toString();
+        return DEFAULT_GENERATOR.toJson(cal);
     }
 
     /**
      * @return the string representation of an uuid
      */
     public static String toJson(UUID uuid) {
-        CharBuf buffer = CharBuf.create(64);
-        writeObject(uuid, buffer); // checking null inside
-
-        return buffer.toString();
+        return DEFAULT_GENERATOR.toJson(uuid);
     }
 
     /**
      * @return the string representation of the URL
      */
     public static String toJson(URL url) {
-        CharBuf buffer = CharBuf.create(64);
-        writeObject(url, buffer); // checking null inside
-
-        return buffer.toString();
+        return DEFAULT_GENERATOR.toJson(url);
     }
 
     /**
      * @return an object representation of a closure
      */
     public static String toJson(Closure closure) {
-        if (closure == null) {
-            return NULL_VALUE;
-        }
-
-        CharBuf buffer = CharBuf.create(255);
-        writeMap(JsonDelegate.cloneDelegateAndGetContent(closure), buffer);
-
-        return buffer.toString();
+        return DEFAULT_GENERATOR.toJson(closure);
     }
 
     /**
      * @return an object representation of an Expando
      */
     public static String toJson(Expando expando) {
-        if (expando == null) {
-            return NULL_VALUE;
-        }
-
-        CharBuf buffer = CharBuf.create(255);
-        writeMap(expando.getProperties(), buffer);
-
-        return buffer.toString();
+        return DEFAULT_GENERATOR.toJson(expando);
     }
 
     /**
@@ -199,289 +144,14 @@ public class JsonOutput {
      * or representation for other object.
      */
     public static String toJson(Object object) {
-        CharBuf buffer = CharBuf.create(255);
-        writeObject(object, buffer); // checking null inside
-
-        return buffer.toString();
+        return DEFAULT_GENERATOR.toJson(object);
     }
 
     /**
      * @return a JSON object representation for a map
      */
     public static String toJson(Map m) {
-        if (m == null) {
-            return NULL_VALUE;
-        }
-
-        CharBuf buffer = CharBuf.create(255);
-        writeMap(m, buffer);
-
-        return buffer.toString();
-    }
-
-    /**
-     * Serializes Number value and writes it into specified buffer.
-     */
-    private static void writeNumber(Class<?> numberClass, Number value, CharBuf buffer) {
-        if (numberClass == Integer.class) {
-            buffer.addInt((Integer) value);
-        } else if (numberClass == Long.class) {
-            buffer.addLong((Long) value);
-        } else if (numberClass == BigInteger.class) {
-            buffer.addBigInteger((BigInteger) value);
-        } else if (numberClass == BigDecimal.class) {
-            buffer.addBigDecimal((BigDecimal) value);
-        } else if (numberClass == Double.class) {
-            Double doubleValue = (Double) value;
-            if (doubleValue.isInfinite()) {
-                throw new JsonException("Number " + value + " can't be serialized as JSON: infinite are not allowed in JSON.");
-            }
-            if (doubleValue.isNaN()) {
-                throw new JsonException("Number " + value + " can't be serialized as JSON: NaN are not allowed in JSON.");
-            }
-
-            buffer.addDouble(doubleValue);
-        } else if (numberClass == Float.class) {
-            Float floatValue = (Float) value;
-            if (floatValue.isInfinite()) {
-                throw new JsonException("Number " + value + " can't be serialized as JSON: infinite are not allowed in JSON.");
-            }
-            if (floatValue.isNaN()) {
-                throw new JsonException("Number " + value + " can't be serialized as JSON: NaN are not allowed in JSON.");
-            }
-
-            buffer.addFloat(floatValue);
-        } else if (numberClass == Byte.class) {
-            buffer.addByte((Byte) value);
-        } else if (numberClass == Short.class) {
-            buffer.addShort((Short) value);
-        } else { // Handle other Number implementations
-            buffer.addString(value.toString());
-        }
-    }
-
-    /**
-     * Serializes object and writes it into specified buffer.
-     */
-    private static void writeObject(Object object, CharBuf buffer) {
-        if (object == null) {
-            buffer.addNull();
-        } else {
-            Class<?> objectClass = object.getClass();
-
-            if (CharSequence.class.isAssignableFrom(objectClass)) { // Handle String, StringBuilder, GString and other CharSequence implementations
-                writeCharSequence((CharSequence) object, buffer);
-            } else if (objectClass == Boolean.class) {
-                buffer.addBoolean((Boolean) object);
-            } else if (Number.class.isAssignableFrom(objectClass)) {
-                writeNumber(objectClass, (Number) object, buffer);
-            } else if (Date.class.isAssignableFrom(objectClass)) {
-                writeDate((Date) object, buffer);
-            } else if (Calendar.class.isAssignableFrom(objectClass)) {
-                writeDate(((Calendar) object).getTime(), buffer);
-            } else if (Map.class.isAssignableFrom(objectClass)) {
-                writeMap((Map) object, buffer);
-            } else if (Iterable.class.isAssignableFrom(objectClass)) {
-                writeIterator(((Iterable<?>) object).iterator(), buffer);
-            } else if (Iterator.class.isAssignableFrom(objectClass)) {
-                writeIterator((Iterator) object, buffer);
-            } else if (objectClass == Character.class) {
-                buffer.addJsonEscapedString(Chr.array((Character) object));
-            } else if (objectClass == URL.class) {
-                buffer.addJsonEscapedString(object.toString());
-            } else if (objectClass == UUID.class) {
-                buffer.addQuoted(object.toString());
-            } else if (objectClass == JsonUnescaped.class) {
-                buffer.add(object.toString());
-            } else if (Closure.class.isAssignableFrom(objectClass)) {
-                writeMap(JsonDelegate.cloneDelegateAndGetContent((Closure<?>) object), buffer);
-            } else if (Expando.class.isAssignableFrom(objectClass)) {
-                writeMap(((Expando) object).getProperties(), buffer);
-            } else if (Enumeration.class.isAssignableFrom(objectClass)) {
-                List<?> list = Collections.list((Enumeration<?>) object);
-                writeIterator(list.iterator(), buffer);
-            } else if (objectClass.isArray()) {
-                writeArray(objectClass, object, buffer);
-            } else if (Enum.class.isAssignableFrom(objectClass)) {
-                buffer.addQuoted(((Enum<?>) object).name());
-            }else if (File.class.isAssignableFrom(objectClass)){
-                Map<?, ?> properties = getObjectProperties(object);
-                //Clean up all recursive references to File objects
-                Iterator<? extends Map.Entry<?, ?>> iterator = properties.entrySet().iterator();
-                while(iterator.hasNext()){
-                    Map.Entry<?,?> entry = iterator.next();
-                    if(entry.getValue() instanceof File){
-                        iterator.remove();
-                    }
-                }
-
-                writeMap(properties, buffer);
-            } else {
-                Map<?, ?> properties = getObjectProperties(object);
-                writeMap(properties, buffer);
-            }
-        }
-    }
-
-    private static Map<?, ?> getObjectProperties(Object object) {
-        Map<?, ?> properties = DefaultGroovyMethods.getProperties(object);
-        properties.remove("class");
-        properties.remove("declaringClass");
-        properties.remove("metaClass");
-        return properties;
-    }
-
-
-    /**
-     * Serializes any char sequence and writes it into specified buffer.
-     */
-    private static void writeCharSequence(CharSequence seq, CharBuf buffer) {
-        if (seq.length() > 0) {
-            buffer.addJsonEscapedString(seq.toString());
-        } else {
-            buffer.addChars(EMPTY_STRING_CHARS);
-        }
-    }
-
-    /**
-     * Serializes date and writes it into specified buffer.
-     */
-    private static void writeDate(Date date, CharBuf buffer) {
-        SimpleDateFormat formatter = new SimpleDateFormat(JSON_DATE_FORMAT, Locale.US);
-        formatter.setTimeZone(TimeZone.getTimeZone(DEFAULT_TIMEZONE));
-        buffer.addQuoted(formatter.format(date));
-    }
-
-    /**
-     * Serializes array and writes it into specified buffer.
-     */
-    private static void writeArray(Class<?> arrayClass, Object array, CharBuf buffer) {
-        buffer.addChar(OPEN_BRACKET);
-        if (Object[].class.isAssignableFrom(arrayClass)) {
-            Object[] objArray = (Object[]) array;
-            if (objArray.length > 0) {
-                writeObject(objArray[0], buffer);
-                for (int i = 1; i < objArray.length; i++) {
-                    buffer.addChar(COMMA);
-                    writeObject(objArray[i], buffer);
-                }
-            }
-        } else if (int[].class.isAssignableFrom(arrayClass)) {
-            int[] intArray = (int[]) array;
-            if (intArray.length > 0) {
-                buffer.addInt(intArray[0]);
-                for (int i = 1; i < intArray.length; i++) {
-                    buffer.addChar(COMMA).addInt(intArray[i]);
-                }
-            }
-        } else if (long[].class.isAssignableFrom(arrayClass)) {
-            long[] longArray = (long[]) array;
-            if (longArray.length > 0) {
-                buffer.addLong(longArray[0]);
-                for (int i = 1; i < longArray.length; i++) {
-                    buffer.addChar(COMMA).addLong(longArray[i]);
-                }
-            }
-        } else if (boolean[].class.isAssignableFrom(arrayClass)) {
-            boolean[] booleanArray = (boolean[]) array;
-            if (booleanArray.length > 0) {
-                buffer.addBoolean(booleanArray[0]);
-                for (int i = 1; i < booleanArray.length; i++) {
-                    buffer.addChar(COMMA).addBoolean(booleanArray[i]);
-                }
-            }
-        } else if (char[].class.isAssignableFrom(arrayClass)) {
-            char[] charArray = (char[]) array;
-            if (charArray.length > 0) {
-                buffer.addJsonEscapedString(Chr.array(charArray[0]));
-                for (int i = 1; i < charArray.length; i++) {
-                    buffer.addChar(COMMA).addJsonEscapedString(Chr.array(charArray[i]));
-                }
-            }
-        } else if (double[].class.isAssignableFrom(arrayClass)) {
-            double[] doubleArray = (double[]) array;
-            if (doubleArray.length > 0) {
-                buffer.addDouble(doubleArray[0]);
-                for (int i = 1; i < doubleArray.length; i++) {
-                    buffer.addChar(COMMA).addDouble(doubleArray[i]);
-                }
-            }
-        } else if (float[].class.isAssignableFrom(arrayClass)) {
-            float[] floatArray = (float[]) array;
-            if (floatArray.length > 0) {
-                buffer.addFloat(floatArray[0]);
-                for (int i = 1; i < floatArray.length; i++) {
-                    buffer.addChar(COMMA).addFloat(floatArray[i]);
-                }
-            }
-        } else if (byte[].class.isAssignableFrom(arrayClass)) {
-            byte[] byteArray = (byte[]) array;
-            if (byteArray.length > 0) {
-                buffer.addByte(byteArray[0]);
-                for (int i = 1; i < byteArray.length; i++) {
-                    buffer.addChar(COMMA).addByte(byteArray[i]);
-                }
-            }
-        } else if (short[].class.isAssignableFrom(arrayClass)) {
-            short[] shortArray = (short[]) array;
-            if (shortArray.length > 0) {
-                buffer.addShort(shortArray[0]);
-                for (int i = 1; i < shortArray.length; i++) {
-                    buffer.addChar(COMMA).addShort(shortArray[i]);
-                }
-            }
-        }
-        buffer.addChar(CLOSE_BRACKET);
-    }
-
-    private static final char[] EMPTY_MAP_CHARS = {OPEN_BRACE, CLOSE_BRACE};
-
-    /**
-     * Serializes map and writes it into specified buffer.
-     */
-    private static void writeMap(Map<?, ?> map, CharBuf buffer) {
-        if (!map.isEmpty()) {
-            buffer.addChar(OPEN_BRACE);
-            boolean firstItem = true;
-            for (Map.Entry<?, ?> entry : map.entrySet()) {
-                if (entry.getKey() == null) {
-                    throw new IllegalArgumentException("Maps with null keys can\'t be converted to JSON");
-                }
-
-                if (!firstItem) {
-                    buffer.addChar(COMMA);
-                } else {
-                    firstItem = false;
-                }
-
-                buffer.addJsonFieldName(entry.getKey().toString());
-                writeObject(entry.getValue(), buffer);
-            }
-            buffer.addChar(CLOSE_BRACE);
-        } else {
-            buffer.addChars(EMPTY_MAP_CHARS);
-        }
-    }
-
-    private static final char[] EMPTY_LIST_CHARS = {OPEN_BRACKET, CLOSE_BRACKET};
-
-    /**
-     * Serializes iterator and writes it into specified buffer.
-     */
-    private static void writeIterator(Iterator<?> iterator, CharBuf buffer) {
-        if (iterator.hasNext()) {
-            buffer.addChar(OPEN_BRACKET);
-            Object it = iterator.next();
-            writeObject(it, buffer);
-            while (iterator.hasNext()) {
-                it = iterator.next();
-                buffer.addChar(COMMA);
-                writeObject(it, buffer);
-            }
-            buffer.addChar(CLOSE_BRACKET);
-        } else {
-            buffer.addChars(EMPTY_LIST_CHARS);
-        }
+        return DEFAULT_GENERATOR.toJson(m);
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/main/java/groovy/json/StreamingJsonBuilder.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/groovy/json/StreamingJsonBuilder.java b/subprojects/groovy-json/src/main/java/groovy/json/StreamingJsonBuilder.java
index e52986f..69d5173 100644
--- a/subprojects/groovy-json/src/main/java/groovy/json/StreamingJsonBuilder.java
+++ b/subprojects/groovy-json/src/main/java/groovy/json/StreamingJsonBuilder.java
@@ -73,6 +73,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
     private static final String COLON_WITH_OPEN_BRACE = ":{";
 
     private final Writer writer;
+    private final JsonGenerator generator;
 
     /**
      * Instantiates a JSON builder.
@@ -81,6 +82,19 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
      */
     public StreamingJsonBuilder(Writer writer) {
         this.writer = writer;
+        generator = JsonOutput.DEFAULT_GENERATOR;
+    }
+
+    /**
+     * Instantiates a JSON builder with the given generator.
+     *
+     * @param writer A writer to which Json will be written
+     * @param generator used to generate the output
+     * @since 2.5
+     */
+    public StreamingJsonBuilder(Writer writer, JsonGenerator generator) {
+        this.writer = writer;
+        this.generator = generator;
     }
 
     /**
@@ -88,11 +102,27 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
      *
      * @param writer  A writer to which Json will be written
      * @param content a pre-existing data structure, default to null
+     * @throws IOException
      */
     public StreamingJsonBuilder(Writer writer, Object content) throws IOException {
-        this(writer);
+        this(writer, content, JsonOutput.DEFAULT_GENERATOR);
+    }
+
+    /**
+     * Instantiates a JSON builder, possibly with some existing data structure and
+     * the given generator.
+     *
+     * @param writer A writer to which Json will be written
+     * @param content a pre-existing data structure, default to null
+     * @param generator used to generate the output
+     * @throws IOException
+     * @since 2.5
+     */
+    public StreamingJsonBuilder(Writer writer, Object content, JsonGenerator generator) throws IOException {
+        this.writer = writer;
+        this.generator = generator;
         if (content != null) {
-            writer.write(JsonOutput.toJson(content));
+            writer.write(generator.toJson(content));
         }
     }
 
@@ -113,7 +143,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
      * @return a map of key / value pairs
      */
     public Object call(Map m) throws IOException {
-        writer.write(JsonOutput.toJson(m));
+        writer.write(generator.toJson(m));
 
         return m;
     }
@@ -133,7 +163,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
      * @throws IOException
      */
     public void call(String name) throws IOException {
-        writer.write(JsonOutput.toJson(Collections.singletonMap(name, Collections.emptyMap())));
+        writer.write(generator.toJson(Collections.singletonMap(name, Collections.emptyMap())));
     }
 
     /**
@@ -154,7 +184,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
      * @return a list of values
      */
     public Object call(List l) throws IOException {
-        writer.write(JsonOutput.toJson(l));
+        writer.write(generator.toJson(l));
 
         return l;
     }
@@ -204,7 +234,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
      * @param c a closure used to convert the objects of coll
      */
     public Object call(Iterable coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
-        return StreamingJsonDelegate.writeCollectionWithClosure(writer, coll, c);
+        return StreamingJsonDelegate.writeCollectionWithClosure(writer, coll, c, generator);
     }
 
     /**
@@ -234,7 +264,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
      */
     public Object call(@DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
         writer.write(JsonOutput.OPEN_BRACE);
-        StreamingJsonDelegate.cloneDelegateAndGetContent(writer, c);
+        StreamingJsonDelegate.cloneDelegateAndGetContent(writer, c, true, generator);
         writer.write(JsonOutput.CLOSE_BRACE);
 
         return null;
@@ -261,7 +291,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
      */
     public void call(String name, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
         writer.write(JsonOutput.OPEN_BRACE);
-        writer.write(JsonOutput.toJson(name));
+        writer.write(generator.toJson(name));
         writer.write(JsonOutput.COLON);
         call(c);
         writer.write(JsonOutput.CLOSE_BRACE);
@@ -292,7 +322,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
      */
     public void call(String name, Iterable coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
         writer.write(JsonOutput.OPEN_BRACE);
-        writer.write(JsonOutput.toJson(name));
+        writer.write(generator.toJson(name));
         writer.write(JsonOutput.COLON);
         call(coll, c);
         writer.write(JsonOutput.CLOSE_BRACE);
@@ -329,7 +359,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
      */
     public void call(String name, Map map, @DelegatesTo(StreamingJsonDelegate.class) Closure callable) throws IOException {
         writer.write(JsonOutput.OPEN_BRACE);
-        writer.write(JsonOutput.toJson(name));
+        writer.write(generator.toJson(name));
         writer.write(COLON_WITH_OPEN_BRACE);
         boolean first = true;
         for (Object it : map.entrySet()) {
@@ -340,11 +370,19 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
             }
 
             Map.Entry entry = (Map.Entry) it;
-            writer.write(JsonOutput.toJson(entry.getKey()));
+            String key = entry.getKey().toString();
+            if (generator.isExcludingFieldsNamed(key)) {
+                continue;
+            }
+            Object value = entry.getValue();
+            if (generator.isExcludingValues(value)) {
+                return;
+            }
+            writer.write(generator.toJson(key));
             writer.write(JsonOutput.COLON);
-            writer.write(JsonOutput.toJson(entry.getValue()));
+            writer.write(generator.toJson(value));
         }
-        StreamingJsonDelegate.cloneDelegateAndGetContent(writer, callable, map.size() == 0);
+        StreamingJsonDelegate.cloneDelegateAndGetContent(writer, callable, map.size() == 0, generator);
         writer.write(DOUBLE_CLOSE_BRACKET);
     }
 
@@ -479,10 +517,16 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
         protected boolean first;
         protected State state;
 
+        private final JsonGenerator generator;
 
         public StreamingJsonDelegate(Writer w, boolean first) {
+            this(w, first, null);
+        }
+
+        StreamingJsonDelegate(Writer w, boolean first, JsonGenerator generator) {
             this.writer = w;
             this.first = first;
+            this.generator = (generator != null) ? generator : JsonOutput.DEFAULT_GENERATOR;
         }
 
         /**
@@ -548,6 +592,9 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
          * @throws IOException
          */
         public void call(String name, List<Object> list) throws IOException {
+            if (generator.isExcludingFieldsNamed(name)) {
+                return;
+            }
             writeName(name);
             writeArray(list);
         }
@@ -559,6 +606,9 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
          * @throws IOException
          */
         public void call(String name, Object...array) throws IOException {
+            if (generator.isExcludingFieldsNamed(name)) {
+                return;
+            }
             writeName(name);
             writeArray(Arrays.asList(array));
         }
@@ -589,6 +639,9 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
          * @param c a closure used to convert the objects of coll
          */
         public void call(String name, Iterable coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
+            if (generator.isExcludingFieldsNamed(name)) {
+                return;
+            }
             writeName(name);
             writeObjects(coll, c);
         }
@@ -608,6 +661,9 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
          * @throws IOException
          */
         public void call(String name, Object value) throws IOException {
+            if (generator.isExcludingFieldsNamed(name) || generator.isExcludingValues(value)) {
+                return;
+            }
             writeName(name);
             writeValue(value);
         }
@@ -620,9 +676,12 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
          * @throws IOException
          */
         public void call(String name, Object value, @DelegatesTo(StreamingJsonDelegate.class) Closure callable) throws IOException {
+            if (generator.isExcludingFieldsNamed(name)) {
+                return;
+            }
             writeName(name);
             verifyValue();
-            writeObject(writer, value, callable);
+            writeObject(writer, value, callable, generator);
         }
         /**
          * Writes the name and another JSON object
@@ -632,10 +691,13 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
          * @throws IOException
          */
         public void call(String name,@DelegatesTo(StreamingJsonDelegate.class) Closure value) throws IOException {
+            if (generator.isExcludingFieldsNamed(name)) {
+                return;
+            }
             writeName(name);
             verifyValue();
             writer.write(JsonOutput.OPEN_BRACE);
-            StreamingJsonDelegate.cloneDelegateAndGetContent(writer, value);
+            StreamingJsonDelegate.cloneDelegateAndGetContent(writer, value, true, generator);
             writer.write(JsonOutput.CLOSE_BRACE);
 
         }
@@ -647,6 +709,9 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
          * @throws IOException
          */
         public void call(String name, JsonOutput.JsonUnescaped json) throws IOException {
+            if (generator.isExcludingFieldsNamed(name)) {
+                return;
+            }
             writeName(name);
             verifyValue();
             writer.write(json.toString());
@@ -672,7 +737,7 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
 
         private void writeObjects(Iterable coll, @DelegatesTo(StreamingJsonDelegate.class) Closure c) throws IOException {
             verifyValue();
-            writeCollectionWithClosure(writer, coll, c);
+            writeCollectionWithClosure(writer, coll, c, generator);
         }
 
         protected void verifyValue() {
@@ -686,6 +751,9 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
 
 
         protected void writeName(String name) throws IOException {
+            if (generator.isExcludingFieldsNamed(name)) {
+                return;
+            }
             if(state == State.NAME) {
                 throw new IllegalStateException("Cannot write a name when a name has just been written. Write a value first!");
             }
@@ -697,18 +765,21 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
             } else {
                 first = false;
             }
-            writer.write(JsonOutput.toJson(name));
+            writer.write(generator.toJson(name));
             writer.write(JsonOutput.COLON);
         }
 
         protected void writeValue(Object value) throws IOException {
+            if (generator.isExcludingValues(value)) {
+                return;
+            }
             verifyValue();
-            writer.write(JsonOutput.toJson(value));
+            writer.write(generator.toJson(value));
         }
 
         protected void writeArray(List<Object> list) throws IOException {
             verifyValue();
-            writer.write(JsonOutput.toJson(list));
+            writer.write(generator.toJson(list));
         }
 
         public static boolean isCollectionWithClosure(Object[] args) {
@@ -716,10 +787,11 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
         }
 
         public static Object writeCollectionWithClosure(Writer writer, Collection coll, @DelegatesTo(StreamingJsonDelegate.class) Closure closure) throws IOException {
-            return writeCollectionWithClosure(writer, (Iterable)coll, closure);
+            return writeCollectionWithClosure(writer, (Iterable)coll, closure, JsonOutput.DEFAULT_GENERATOR);
         }
 
-        public static Object writeCollectionWithClosure(Writer writer, Iterable coll, @DelegatesTo(StreamingJsonDelegate.class) Closure closure) throws IOException {
+        private static Object writeCollectionWithClosure(Writer writer, Iterable coll, @DelegatesTo(StreamingJsonDelegate.class) Closure closure, JsonGenerator generator)
+                throws IOException {
             writer.write(JsonOutput.OPEN_BRACKET);
             boolean first = true;
             for (Object it : coll) {
@@ -729,16 +801,16 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
                     first = false;
                 }
 
-                writeObject(writer, it, closure);
+                writeObject(writer, it, closure, generator);
             }
             writer.write(JsonOutput.CLOSE_BRACKET);
 
             return writer;
         }
 
-        private static void writeObject(Writer writer, Object object, Closure closure) throws IOException {
+        private static void writeObject(Writer writer, Object object, Closure closure, JsonGenerator generator) throws IOException {
             writer.write(JsonOutput.OPEN_BRACE);
-            curryDelegateAndGetContent(writer, closure, object);
+            curryDelegateAndGetContent(writer, closure, object, true, generator);
             writer.write(JsonOutput.CLOSE_BRACE);
         }
 
@@ -748,7 +820,11 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
         }
 
         public static void cloneDelegateAndGetContent(Writer w, @DelegatesTo(StreamingJsonDelegate.class) Closure c, boolean first) {
-            StreamingJsonDelegate delegate = new StreamingJsonDelegate(w, first);
+            cloneDelegateAndGetContent(w, c, first, JsonOutput.DEFAULT_GENERATOR);
+        }
+
+        private static void cloneDelegateAndGetContent(Writer w, @DelegatesTo(StreamingJsonDelegate.class) Closure c, boolean first, JsonGenerator generator) {
+            StreamingJsonDelegate delegate = new StreamingJsonDelegate(w, first, generator);
             Closure cloned = (Closure) c.clone();
             cloned.setDelegate(delegate);
             cloned.setResolveStrategy(Closure.DELEGATE_FIRST);
@@ -760,7 +836,11 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
         }
 
         public static void curryDelegateAndGetContent(Writer w, @DelegatesTo(StreamingJsonDelegate.class) Closure c, Object o, boolean first) {
-            StreamingJsonDelegate delegate = new StreamingJsonDelegate(w, first);
+            curryDelegateAndGetContent(w, c, o, first, JsonOutput.DEFAULT_GENERATOR);
+        }
+
+        private static void curryDelegateAndGetContent(Writer w, @DelegatesTo(StreamingJsonDelegate.class) Closure c, Object o, boolean first, JsonGenerator generator) {
+            StreamingJsonDelegate delegate = new StreamingJsonDelegate(w, first, generator);
             Closure curried = c.curry(o);
             curried.setDelegate(delegate);
             curried.setResolveStrategy(Closure.DELEGATE_FIRST);
@@ -772,5 +852,3 @@ public class StreamingJsonBuilder extends GroovyObjectSupport {
         }
     }
 }
-
-

http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/main/java/groovy/json/internal/CharBuf.java
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/main/java/groovy/json/internal/CharBuf.java b/subprojects/groovy-json/src/main/java/groovy/json/internal/CharBuf.java
index feaa614..18f5d6a 100644
--- a/subprojects/groovy-json/src/main/java/groovy/json/internal/CharBuf.java
+++ b/subprojects/groovy-json/src/main/java/groovy/json/internal/CharBuf.java
@@ -341,32 +341,34 @@ public class CharBuf extends Writer implements CharSequence {
     }
 
     public final CharBuf addJsonEscapedString(String jsonString) {
+        return addJsonEscapedString(jsonString, false);
+    }
+
+    public final CharBuf addJsonEscapedString(String jsonString, boolean disableUnicodeEscaping) {
         char[] charArray = FastStringUtils.toCharArray(jsonString);
-        return addJsonEscapedString(charArray);
+        return addJsonEscapedString(charArray, disableUnicodeEscaping);
     }
 
-    private static boolean hasAnyJSONControlOrUnicodeChars(int c) {
-        /* Anything less than space is a control character. */
-        if (c < 30) {
+    private static boolean shouldEscape(int c, boolean disableUnicodeEscaping) {
+        if (c < 32) { /* less than space is a control char */
             return true;
-        /* 34 is double quote. */
-        } else if (c == 34) {
+        } else if (c == 34) {  /* double quote */
             return true;
-        } else if (c == 92) {
+        } else if (c == 92) {  /* backslash */
             return true;
-        } else if (c < ' ' || c > 126) {
+        } else if (!disableUnicodeEscaping && c > 126) {  /* non-ascii char range */
             return true;
         }
 
         return false;
     }
 
-    private static boolean hasAnyJSONControlChars(final char[] charArray) {
+    private static boolean hasAnyJSONControlChars(final char[] charArray, boolean disableUnicodeEscaping) {
         int index = 0;
         char c;
         while (true) {
             c = charArray[index];
-            if (hasAnyJSONControlOrUnicodeChars(c)) {
+            if (shouldEscape(c, disableUnicodeEscaping)) {
                 return true;
             }
             if (++index >= charArray.length) return false;
@@ -374,9 +376,13 @@ public class CharBuf extends Writer implements CharSequence {
     }
 
     public final CharBuf addJsonEscapedString(final char[] charArray) {
+        return addJsonEscapedString(charArray, false);
+    }
+
+    public final CharBuf addJsonEscapedString(final char[] charArray, boolean disableUnicodeEscaping) {
         if (charArray.length == 0) return this;
-        if (hasAnyJSONControlChars(charArray)) {
-            return doAddJsonEscapedString(charArray);
+        if (hasAnyJSONControlChars(charArray, disableUnicodeEscaping)) {
+            return doAddJsonEscapedString(charArray, disableUnicodeEscaping);
         } else {
             return this.addQuoted(charArray);
         }
@@ -386,7 +392,7 @@ public class CharBuf extends Writer implements CharSequence {
 
     final byte[] charTo = new byte[2];
 
-    private CharBuf doAddJsonEscapedString(char[] charArray) {
+    private CharBuf doAddJsonEscapedString(char[] charArray, boolean disableUnicodeEscaping) {
         char[] _buffer = buffer;
         int _location = this.location;
 
@@ -410,7 +416,7 @@ public class CharBuf extends Writer implements CharSequence {
         while (true) {
             char c = charArray[index];
 
-            if (hasAnyJSONControlOrUnicodeChars(c)) {
+            if (shouldEscape(c, disableUnicodeEscaping)) {
                    /* We are covering our bet with a safety net.
                       otherwise we would have to have 5x buffer
                       allocated for control chars */
@@ -514,14 +520,22 @@ public class CharBuf extends Writer implements CharSequence {
     }
 
     public final CharBuf addJsonFieldName(String str) {
-        return addJsonFieldName(FastStringUtils.toCharArray(str));
+        return addJsonFieldName(str, false);
+    }
+
+    public final CharBuf addJsonFieldName(String str, boolean disableUnicodeEscaping) {
+        return addJsonFieldName(FastStringUtils.toCharArray(str), disableUnicodeEscaping);
     }
 
     private static final char[] EMPTY_STRING_CHARS = Chr.array('"', '"');
 
     public final CharBuf addJsonFieldName(char[] chars) {
+        return addJsonFieldName(chars, false);
+    }
+
+    public final CharBuf addJsonFieldName(char[] chars, boolean disableUnicodeEscaping) {
         if (chars.length > 0) {
-            addJsonEscapedString(chars);
+            addJsonEscapedString(chars, disableUnicodeEscaping);
         } else {
             addChars(EMPTY_STRING_CHARS);
         }
@@ -671,7 +685,16 @@ public class CharBuf extends Writer implements CharSequence {
     }
 
     public void removeLastChar() {
-        location--;
+        if (location > 0) {
+            location--;
+        }
+    }
+
+    public void removeLastChar(char expect) {
+        if (location == 0 || buffer[location-1] != expect) {
+            return;
+        }
+        removeLastChar();
     }
 
     private Cache<BigDecimal, char[]> bigDCache;

http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/spec/doc/json-builder.adoc
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/spec/doc/json-builder.adoc b/subprojects/groovy-json/src/spec/doc/json-builder.adoc
index dcf21d4..28ffd58 100644
--- a/subprojects/groovy-json/src/spec/doc/json-builder.adoc
+++ b/subprojects/groovy-json/src/spec/doc/json-builder.adoc
@@ -40,4 +40,11 @@ We use https://github.com/lukas-krecan/JsonUnit[JsonUnit] to check that the buil
 [source,groovy]
 ----
 include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/JsonBuilderTest.groovy[tags=json_assert,indent=0]
-----
\ No newline at end of file
+----
+
+If you need to customize the generated output you can pass a `JsonGenerator` instance when creating a `JsonBuilder`:
+
+[source,groovy]
+----
+include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/JsonBuilderTest.groovy[tags=json_builder_generator,indent=0]
+----

http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/spec/doc/json-userguide.adoc
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/spec/doc/json-userguide.adoc b/subprojects/groovy-json/src/spec/doc/json-userguide.adoc
index 5557568..683e403 100644
--- a/subprojects/groovy-json/src/spec/doc/json-userguide.adoc
+++ b/subprojects/groovy-json/src/spec/doc/json-userguide.adoc
@@ -159,7 +159,7 @@ include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/JsonTest.gr
 <<json-userguide.adoc#json_jsonslurper,JsonSlurper>>, being a JSON parser.
 
 `JsonOutput` comes with overloaded, static `toJson` methods. Each `toJson` implementation takes a different parameter type.
-The static method can either be used directly or by importing the methods with a static import statement.
+The static methods can either be used directly or by importing the methods with a static import statement.
 
 The result of a `toJson` call is a `String` containing the JSON code.
 
@@ -176,6 +176,30 @@ has support for serialising POGOs, that is, plain-old Groovy objects.
 include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/JsonTest.groovy[tags=json_output_pogo,indent=0]
 ----
 
+=== Customizing Output
+
+If you need control over the serialized output you can use a `JsonGenerator`.  The `JsonGenerator.Options` builder
+can be used to create a customized generator.  One or more options can be set on this builder in order to alter
+the resulting output.  When you are done setting the options simply call the `build()` method in order to get a fully
+configured instance that will generate output based on the options selected.
+
+[source,groovy]
+----
+include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/JsonTest.groovy[tags=json_output_generator,indent=0]
+----
+
+A closure can be used to transform a type into a valid JSON value.  These closure converters are registered
+for a given type and will be called any time that type or a subtype is encountered.  The first parameter to the
+closure is an object matching the type for which the converter is registered and this parameter is required.
+The closure may take an optional second `String` parameter and this will be set to the key name if one is available.
+
+[source,groovy]
+----
+include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/JsonTest.groovy[tags=json_output_converter,indent=0]
+----
+
+==== Formatted Output
+
 As we saw in previous examples, the JSON output is not pretty printed per default. However, the `prettyPrint` method in `JsonOutput` comes
 to rescue for this task.
 
@@ -187,6 +211,8 @@ include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/JsonTest.gr
 `prettyPrint` takes a `String` as single parameter; therefore, it can be applied on arbitrary JSON `String` instances, not only the result of
 `JsonOutput.toJson`.
 
+=== Builders
+
 Another way to create JSON from Groovy is to use `JsonBuilder` or `StreamingJsonBuilder`. Both builders provide a
 DSL which allows to formulate an object graph which is then converted to JSON.
 

http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/spec/doc/streaming-jason-builder.adoc
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/spec/doc/streaming-jason-builder.adoc b/subprojects/groovy-json/src/spec/doc/streaming-jason-builder.adoc
index 296794e..98d3e59 100644
--- a/subprojects/groovy-json/src/spec/doc/streaming-jason-builder.adoc
+++ b/subprojects/groovy-json/src/spec/doc/streaming-jason-builder.adoc
@@ -44,4 +44,11 @@ We use https://github.com/lukas-krecan/JsonUnit[JsonUnit] to check the expected
 [source,groovy]
 ----
 include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/StreamingJsonBuilderTest.groovy[tags=json_assert,indent=0]
-----
\ No newline at end of file
+----
+
+If you need to customize the generated output you can pass a `JsonGenerator` instance when creating a `StreamingJsonBuilder`:
+
+[source,groovy]
+----
+include::{rootProjectDir}/subprojects/groovy-json/src/spec/test/json/StreamingJsonBuilderTest.groovy[tags=streaming_json_builder_generator,indent=0]
+----

http://git-wip-us.apache.org/repos/asf/groovy/blob/13202599/subprojects/groovy-json/src/spec/test/json/JsonBuilderTest.groovy
----------------------------------------------------------------------
diff --git a/subprojects/groovy-json/src/spec/test/json/JsonBuilderTest.groovy b/subprojects/groovy-json/src/spec/test/json/JsonBuilderTest.groovy
index 87ab5f1..d4a1576 100644
--- a/subprojects/groovy-json/src/spec/test/json/JsonBuilderTest.groovy
+++ b/subprojects/groovy-json/src/spec/test/json/JsonBuilderTest.groovy
@@ -69,4 +69,36 @@ class JsonBuilderTest extends GroovyTestCase {
             // end::json_assert[]
        """
     }
+
+    void testJsonBuilderWithGenerator() {
+        assertScript """
+            // tag::json_builder_generator[]
+            import groovy.json.*
+
+            def generator = new JsonGenerator.Options()
+                    .excludeNulls()
+                    .excludeFieldsByName('make', 'country', 'record')
+                    .excludeFieldsByType(Number)
+                    .addConverter(URL) { url -> '"http://groovy-lang.org"' }
+                    .build()
+
+            JsonBuilder builder = new JsonBuilder(generator)
+            builder.records {
+              car {
+                    name 'HSV Maloo'
+                    make 'Holden'
+                    year 2006
+                    country 'Australia'
+                    homepage new URL('http://example.org')
+                    record {
+                        type 'speed'
+                        description 'production pickup truck with speed of 271kph'
+                    }
+              }
+            }
+
+            assert builder.toString() == '{"records":{"car":{"name":"HSV Maloo","homepage":"http://groovy-lang.org"}}}'
+            // end::json_builder_generator[]
+        """
+    }
 }