JSON
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Groovy 6.0 (6.0.0, 2026-09-22) and every example was run with Groovy 6.0.0 on JDK 26
(checked on 2026-10-14). JSON is the format most Groovy programs meet first, and the groovy.json package turns it into
plain maps and lists and back with very little code: JsonSlurper reads, JsonOutput and JsonGenerator write,
JsonBuilder and StreamingJsonBuilder describe a document with the builder syntax covered in
Builders. The results of parsing are ordinary collections, so everything on
Working with Collections and Maps
applies to them. The sibling formats are on YAML & TOML and
XML Parsing, GPath & Security; the closure syntax used by
the builders is on Closures.
The groovy-json module
JSON support lives in its own artifact, groovy-json. It is part of the Groovy distribution, so a script run with the
groovy command needs no setup. A project that embeds Groovy declares the dependency, and a script that runs outside the
distribution can use @Grab.
@Grab('org.apache.groovy:groovy-json:6.0.0')
import groovy.json.JsonSlurper
dependencies {
implementation("org.apache.groovy:groovy-json:6.0.0")
}
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy-json</artifactId>
<version>6.0.0</version>
</dependency>
The examples below share one data file from the greenhouse scenario, data/plants.json.
{
"greenhouse": "Backyard East",
"plants": [
{ "name": "tomatoes", "species": "Solanum lycopersicum", "waterEveryHours": 2, "minMoisture": 0.35, "indoor": true, "notes": null },
{ "name": "basil", "species": "Ocimum basilicum", "waterEveryHours": 6, "minMoisture": 0.5, "indoor": true, "notes": "pinch flowers" },
{ "name": "chillies", "species": "Capsicum annuum", "waterEveryHours": 12, "minMoisture": 0.25, "indoor": false, "notes": null }
]
}
Reading JSON with JsonSlurper
JsonSlurper has parseText for a String and parse overloads for a File, a Path, a Reader, an InputStream, a
URL and byte or character arrays. The result is a Map for a JSON object, a List for an array, and the natural JVM type
for a scalar.
import groovy.json.JsonSlurper
def doc = new JsonSlurper().parse(new File('data/plants.json'))
println doc.greenhouse
println doc.plants*.name
println doc.plants.findAll { it.indoor }*.name
println doc.plants.find { it.name == 'basil' }.minMoisture
println doc.plants.sum { it.waterEveryHours }
println doc.plants.notes
println doc.plants[1].containsKey('notes')
println doc.plants[0].containsKey('colour')
println doc.plants[0].colour
Backyard East
[tomatoes, basil, chillies]
[tomatoes, basil]
0.5
20
[null, pinch flowers, null]
true
false
null
Because the result is made of maps and lists, the property syntax, the spread operator and the GDK methods work directly:
doc.plants*.name collects one property from every element. A key that is absent gives null rather than an error, which
is the same as a key whose value is JSON null; use containsKey when the difference matters. A JSON null is the Groovy
null, not a library placeholder object. The top level of the document does
not have to be an object: a bare array parses to a list, and a bare string or number to a String or a number.
How JSON types map to Groovy types
import groovy.json.JsonSlurper
def values = new JsonSlurper().parseText('''
{ "simple": 123, "fraction": 123.66, "exponential": 123e12, "big": 12345678901,
"flag": true, "nothing": null, "text": "x", "list": [1, 2], "map": {"k": 1} }
/* the default parser accepts comments */''')
values.each { key, value ->
println "${key.padRight(12)}${value?.getClass()?.name} = $value"
}
simple java.lang.Integer = 123
fraction java.math.BigDecimal = 123.66
exponential java.math.BigDecimal = 1.23E+14
big java.lang.Long = 12345678901
flag java.lang.Boolean = true
nothing null = null
text java.lang.String = x
list java.util.ArrayList = [1, 2]
map org.apache.groovy.json.internal.LazyMap = [k:1]
| JSON | Groovy |
|---|---|
string |
|
number without fraction or exponent |
|
number with fraction or exponent |
|
object |
a |
array |
|
|
|
|
|
date-like string |
|
Numbers with a fraction parse to BigDecimal, so decimal values such as 0.35 keep their exact digits (see
Numbers & Numeric Literals). The default parser also
accepts /* */ comments, as the example shows, which strict JSON does not allow.
Choosing a parser variant
JsonSlurper delegates to one of four parser implementations, selected by JsonParserType through the type property. The
default is CHAR_BUFFER.
| Constant | Behaviour | Use it for |
|---|---|---|
|
works on the character array and copies sub-arrays ("chopping") for values |
general use |
|
keeps indexes into the original buffer and creates values as late as possible; fastest |
documents up to about 2 MB, short-lived results |
|
the index-overlay parser with a relaxed grammar: comments, bare keys, single quotes |
hand-written configuration |
|
reads through a window over the input with constant memory |
files larger than about 2 MB |
import groovy.json.*
def text = '{"id":"S-7","when":"2026-09-04T10:00:00.000Z","day":"2026-09-04"}'
JsonParserType.values().each { type ->
def map = new JsonSlurper(type: type).parseText(text)
println "${type.name().padRight(17)}${map.getClass().simpleName.padRight(14)}" +
"when: ${map.when.getClass().simpleName.padRight(7)} day: ${map.day.getClass().simpleName}"
}
println "default parser: ${new JsonSlurper().type}"
INDEX_OVERLAY LazyValueMap when: Date day: String
CHARACTER_SOURCE LazyMap when: String day: String
LAX LazyValueMap when: Date day: String
CHAR_BUFFER LazyMap when: String day: String
default parser: CHAR_BUFFER
The map classes differ: CHAR_BUFFER and CHARACTER_SOURCE return LazyMap, while INDEX_OVERLAY and LAX return
LazyValueMap. Both are java.util.Map implementations, so code written against Map is unaffected, but neither is a
LinkedHashMap.
import groovy.json.*
def source = '{"a":1,"list":[1,2]}'
def doc = new JsonSlurper().parseText(source)
doc.b = 2
doc.list << 3
println doc
println doc == [a: 1, list: [1, 2, 3], b: 2]
println doc instanceof Map
println doc instanceof LinkedHashMap
def overlay = new JsonSlurper(type: JsonParserType.INDEX_OVERLAY).parseText(source)
println overlay.getClass().simpleName
try {
overlay.b = 2
} catch (e) {
println e.message
}
println overlay.a // reading is fine
[a:1, list:[1, 2, 3], b:2]
true
true
false
LazyValueMap
Not that kind of map
1
An index-overlay map is effectively read-only: adding a key throws JsonInternalException. If the result is going to be
modified or cached, use the default parser, or copy it first with new LinkedHashMap(map) (a shallow copy). The LAX variant accepts input that the standard grammar rejects.
import groovy.json.*
def relaxed = '''{ name: 'basil', // single quotes, bare keys and a trailing comma
hours: 6, }'''
println new JsonSlurper(type: JsonParserType.LAX).parseText(relaxed)
try {
new JsonSlurper().parseText(relaxed)
} catch (JsonException e) {
println e.message.readLines().first()
}
try {
new JsonSlurper().parseText('{"a": ')
} catch (JsonException e) {
println e.message.readLines().first()
}
try {
new JsonSlurper().parseText('')
} catch (IllegalArgumentException e) {
println e.message
}
[name:basil, hours:6]
expecting '}' or ',' but got current char 'n' with an int value of 110
Unable to determine the current character, it is not a string, number, array, or object
Text must not be null or empty
Syntax errors throw groovy.json.JsonException, and an empty or null string throws IllegalArgumentException, so
configuration loaders usually wrap both. The first line of the message points at the problem; the rest of it echoes the
offending input with a caret, which can be long.
Date-like strings
Two parser variants, INDEX_OVERLAY and LAX, convert strings that look like a full ISO-8601 timestamp into
java.util.Date; CHAR_BUFFER and CHARACTER_SOURCE never do. A date without a time, such as 2026-09-04, stays a
String in all four. In Groovy 6.0 the conversion is controlled by JsonDateHandling, set with setDateHandling.
import groovy.json.*
def text = '{"offset":"2026-09-04T10:00:00+10:00"}'
JsonDateHandling.values().each { handling ->
def value = new JsonSlurper(type: JsonParserType.INDEX_OVERLAY)
.setDateHandling(handling)
.parseText(text).offset
def shown = value instanceof Date ? value.toInstant() : value
println "${handling.name().padRight(17)}${value.getClass().simpleName.padRight(15)}$shown"
}
// the older switch: false keeps the String
def raw = new JsonSlurper(type: JsonParserType.INDEX_OVERLAY).setCheckDates(false).parseText(text)
println raw.offset.getClass().simpleName
STRING String 2026-09-04T10:00:00+10:00
UTIL_DATE Date 2026-09-04T00:00:00Z
INSTANT Instant 2026-09-04T00:00:00Z
OFFSET_DATE_TIME OffsetDateTime 2026-09-04T10:00+10:00
String
-
STRINGkeeps the text,UTIL_DATEis the default for the two converting parsers, and bothDateandInstantdenote an instant and so lose the+10:00offset the document carried. OnlyOFFSET_DATE_TIMEkeeps it. -
The older
setCheckDates(false)still works and meansSTRING, but it is deprecated. -
The value printed for
UTIL_DATEabove is converted withtoInstant()because printing aDateuses the default time zone of the machine.
Limits on untrusted input
A parser that reads a request body or an uploaded file should not trust its size. JsonSlurper has two limits, both
defaulting to 1000: maxNestingDepth (how deeply arrays and objects may be nested) and maxNumberLength (the digits in a
single number). Exceeding either throws JsonException instead of exhausting the stack or the heap.
import groovy.json.*
def slurper = new JsonSlurper()
println "maxNestingDepth: ${slurper.maxNestingDepth}, maxNumberLength: ${slurper.maxNumberLength}"
def attempt = { Closure parse ->
try { println parse().getClass().simpleName } catch (JsonException e) { println e.message.readLines().first() }
}
attempt { slurper.parseText('[' * 1001 + ']' * 1001) }
attempt { slurper.parseText('[' * 1000 + ']' * 1000) }
attempt { new JsonSlurper(maxNestingDepth: 3).parseText('[[[[1]]]]') }
attempt { new JsonSlurper(maxNumberLength: 5).parseText('[1234567]') }
// every parser type enforces it, though the message differs
JsonParserType.values().each { type ->
print "${type.name().padRight(17)}"
attempt { new JsonSlurper(type: type, maxNestingDepth: 3).parseText('[[[[1]]]]') }
}
maxNestingDepth: 1000, maxNumberLength: 1000
Maximum JSON nesting depth of 1000 exceeded
ArrayList
Maximum JSON nesting depth of 3 exceeded
JSON number length of 7 exceeds the maximum of 5
INDEX_OVERLAY Maximum JSON nesting depth of 3 exceeded
CHARACTER_SOURCE Unexpected issue
LAX Maximum JSON nesting depth of 3 exceeded
CHAR_BUFFER Maximum JSON nesting depth of 3 exceeded
Set a lower maxNestingDepth for documents whose shape you know, and keep the overall input size bounded before parsing,
because the limits cap structure, not length. The CHARACTER_SOURCE variant enforces the depth limit but reports it with a
different message.
Writing JSON with JsonOutput
JsonOutput.toJson is static and overloaded for the common types. It writes maps, collections, arrays, numbers, strings,
enums, UUID, URL, Date, Calendar, Expando, records and plain Groovy objects (POGOs), by reading their properties.
The result is compact JSON with no whitespace.
import groovy.json.JsonOutput
class Plant {
String name
int waterEveryHours
String notes
transient String scratch
}
def basil = new Plant(name: 'basil', waterEveryHours: 6, scratch: 'tmp')
println JsonOutput.toJson(basil)
println JsonOutput.toJson([sensor: 'bench-1', values: [21.5, 22, 'n/a', true, null], tags: [], extra: [:]])
println JsonOutput.toJson('plain text')
println JsonOutput.toJson(42)
println JsonOutput.toJson([name: 'Café', quote: 'say "hi"'])
println JsonOutput.toJson(Optional.of(3))
println JsonOutput.toJson(new Date(0))
{"name":"basil","waterEveryHours":6,"notes":null,"scratch":"tmp"}
{"sensor":"bench-1","values":[21.5,22,"n/a",true,null],"tags":[],"extra":{}}
"plain text"
42
{"name":"Caf\u00e9","quote":"say \"hi\""}
{"empty":false,"present":true}
"1970-01-01T00:00:00+0000"
Points to read in that output:
-
Property order follows the order of declaration; map order follows the map.
-
A
nullproperty is written asnull;excludeNulls(below) removes it. -
A
transientfield is written, becauseJsonOutputfollows properties, not Java serialization rules. UseexcludeFieldsByNameto keep a field out. -
Non-ASCII characters are escaped as
éby default, which is valid JSON and safe in any encoding. -
A
Dateis written as an ISO-8601 string in UTC, and ajava.util.Optionalis written as its two bean properties,emptyandpresent, which is almost never what a client wants; unwrap it first.
The java.time types are not special-cased, so they are written as bean graphs. A converter fixes that (see the next
section), and a non-finite number is refused because JSON cannot represent it.
import groovy.json.JsonOutput
import java.time.*
println JsonOutput.toJson(Instant.parse('2026-09-04T10:00:00Z'))
println JsonOutput.toJson([day: LocalDate.of(2026, 9, 4)]).take(70)
try {
JsonOutput.toJson([ratio: Double.NaN])
} catch (groovy.json.JsonException e) {
println e.message
}
{"epochSecond":1788516000,"nano":0}
{"day":{"chronology":{"calendarType":"iso8601","id":"ISO","isoBased":t
Number NaN can't be serialized as JSON: NaN are not allowed in JSON.
Source: Groovy documentation: JsonOutput
Customizing the output with JsonGenerator
JsonGenerator.Options is a fluent builder; build() returns a JsonGenerator that can be kept and reused for any number of
calls. Its options are excludeNulls, excludeFieldsByName, excludeFieldsByType, dateFormat, timezone,
disableUnicodeEscaping and addConverter.
import groovy.json.JsonGenerator
import java.time.Instant
class Reading {
String sensor
Instant at
BigDecimal temperature
String note
String location
}
def reading = new Reading(sensor: 'bench-1', at: Instant.parse('2026-09-04T10:00:00Z'),
temperature: 21.5, location: 'north bench')
def generator = new JsonGenerator.Options()
.excludeNulls()
.excludeFieldsByName('location')
.addConverter(Instant) { Instant instant -> instant.toString() }
.build()
println generator.toJson(reading)
println generator.toJson([reading, [when: Instant.EPOCH]])
{"sensor":"bench-1","at":"2026-09-04T10:00:00Z","temperature":21.5}
[{"sensor":"bench-1","at":"2026-09-04T10:00:00Z","temperature":21.5},{"when":"1970-01-01T00:00:00Z"}]
A converter is a closure registered for a type; it is called for that type and its subtypes. Its first parameter must match
the registered type, and an optional second String parameter receives the property name, or null inside a list.
import groovy.json.JsonGenerator
import java.time.Instant
def generator = new JsonGenerator.Options()
.addConverter(Instant) { Instant instant, String key ->
key == 'at' ? instant.epochSecond : instant.toString()
}
.excludeFieldsByType(BigDecimal)
.build()
println generator.toJson([at: Instant.EPOCH, other: Instant.EPOCH, temperature: 21.5, hours: 6])
println generator.toJson([Instant.EPOCH]) // no key inside a list
try {
new JsonGenerator.Options().addConverter(Date) { Calendar calendar -> }
} catch (IllegalArgumentException e) {
println e.message
}
{"at":0,"other":"1970-01-01T00:00:00Z","hours":6}
["1970-01-01T00:00:00Z"]
Expected first parameter to be of type: class java.util.Date
Dates have their own options. Without them, a Date is written in UTC with a numeric offset.
import groovy.json.*
def planted = Date.from(java.time.Instant.parse('2026-03-01T08:30:00Z'))
println new JsonGenerator.Options().build().toJson([planted: planted])
println new JsonGenerator.Options().dateFormat('yyyy-MM-dd').timezone('UTC').build().toJson([planted: planted])
println new JsonGenerator.Options().dateFormat('EEE d MMM', Locale.FRANCE).timezone('UTC').build().toJson([planted: planted])
println JsonOutput.toJson([name: 'Café'])
println new JsonGenerator.Options().disableUnicodeEscaping().build().toJson([name: 'Café'])
println JsonOutput.toJson([raw: JsonOutput.unescaped('{"already":"json"}')])
{"planted":"2026-03-01T08:30:00+0000"}
{"planted":"2026-03-01"}
{"planted":"dim. 1 mars"}
{"name":"Caf\u00e9"}
{"name":"Café"}
{"raw":{"already":"json"}}
JsonOutput.unescaped marks a string that is already JSON so that it is embedded as is, which is useful for a fragment built
elsewhere and a hazard for untrusted text. A generator can also be passed to the constructor of JsonBuilder and StreamingJsonBuilder.
Pretty-printing
JsonOutput.prettyPrint reformats a JSON string with four-space indentation. It works on any JSON text, not only on the
output of toJson, and takes an optional flag to leave non-ASCII characters unescaped.
import groovy.json.JsonOutput
def json = JsonOutput.toJson([greenhouse: 'East', plants: [[name: 'basil', hours: 6]], empty: [], none: [:]])
println json
println JsonOutput.prettyPrint(json)
println JsonOutput.prettyPrint('{"café":"é"}')
println JsonOutput.prettyPrint('{"café":"é"}', true)
println JsonOutput.prettyPrint('{"not":"valid" "json"}')
{"greenhouse":"East","plants":[{"name":"basil","hours":6}],"empty":[],"none":{}}
{
"greenhouse": "East",
"plants": [
{
"name": "basil",
"hours": 6
}
],
"empty": [
],
"none": {
}
}
{
"caf\u00e9": "\u00e9"
}
{
"café": "é"
}
{
"not": "valid""json"
}
prettyPrint is a text re-flow, not a validator: the last line shows that invalid input is reformatted without complaint.
Empty arrays and objects are printed across several lines.
Building JSON with JsonBuilder
JsonBuilder collects a structure built with the builder syntax and writes it on demand with toString() or
toPrettyString(). Each call inside the closure becomes a property, a nested closure becomes a nested object, and a call
that takes a collection becomes an array; name(collection) { element -> ... } builds one object per element.
import groovy.json.JsonBuilder
def stock = [[name: 'basil', hours: 6, tags: ['herb']], [name: 'tomatoes', hours: 2, tags: []]]
def json = new JsonBuilder()
json.greenhouse {
name 'Backyard East'
open true
schedule stock.collect { [plant: it.name, everyHours: it.hours] }
zones(stock) { plant ->
id plant.name.toUpperCase()
tagCount plant.tags.size()
}
contact {
nickname 'gardener'
}
}
println json.toString()
println json.toPrettyString().readLines().take(8).join('\n')
println json.content.getClass().simpleName
println json.content.greenhouse.name
{"greenhouse":{"name":"Backyard East","open":true,"schedule":[{"plant":"basil","everyHours":6},{"plant":"tomatoes","everyHours":2}],"zones":[{"id":"BASIL","tagCount":1},{"id":"TOMATOES","tagCount":0}],"contact":{"nickname":"gardener"}}}
{
"greenhouse": {
"name": "Backyard East",
"open": true,
"schedule": [
{
"plant": "basil",
"everyHours": 6
LinkedHashMap
Backyard East
The content is held as a map in content, which can still be changed before the document is written. The builder also
accepts a root array or map, names that are not identifiers, computed names and a configured generator.
import groovy.json.*
// a root array, a map, and names that are not identifiers
def list = new JsonBuilder()
list([1, 2, 3])
println list
def fromMap = new JsonBuilder([a: 1, b: [c: 2]])
println fromMap
def odd = new JsonBuilder()
def key = 'dynamic'
odd {
"needs quotes" 1
"$key" 2
delegate.'class' 'x'
nothing null
}
println odd
// a generator applies to builders too
def trimmed = new JsonBuilder(new JsonGenerator.Options().excludeNulls().build())
trimmed { x null; y 1 }
println trimmed
// content stays editable until it is written
def edited = new JsonBuilder([a: 1])
edited.content.b = 2
println edited
[1,2,3]
{"a":1,"b":{"c":2}}
{"needs quotes":1,"dynamic":2,"class":"x","nothing":null}
{"y":1}
{"a":1,"b":2}
One trap comes from the builder syntax itself: a node name is a method call, so a local variable with the same name as a node is used as the receiver of the call instead.
import groovy.json.JsonBuilder
def plants = [[name: 'basil'], [name: 'mint']]
def json = new JsonBuilder()
try {
json.catalogue {
plants plants.collect { it.name } // plants is also a local variable
}
} catch (MissingMethodException e) {
println e.message.readLines().first().take(110)
}
def fixed = new JsonBuilder()
fixed.catalogue {
delegate.plants plants.collect { it.name } // or rename the local variable
}
println fixed
No signature of method: call for class: java.util.ArrayList is applicable for argument types: (ArrayList) valu
{"catalogue":{"plants":["basil","mint"]}}
The first form fails because plants plants.collect { ... } is read as calling the variable plants with an argument. Writing
delegate.plants, or choosing different names for variables and nodes, fixes it. The explicit delegate. receiver is also how a node
called class, which would otherwise be read as the class property, is written in the previous example. How a builder closure
resolves such names is on
Closure Delegation: Owner, Delegate & This.
Streaming large documents with StreamingJsonBuilder
StreamingJsonBuilder has the same syntax but writes to a Writer as it goes instead of building a map first, so the
memory used does not grow with the document. The price is that the output is already written when a call returns, so
nothing can be edited afterwards. It takes the same optional JsonGenerator.
import groovy.json.*
def out = new StringWriter()
def stream = new StreamingJsonBuilder(out)
stream.reading {
sensor 'bench-1'
values([21.5, 22.0])
meta {
unit 'C'
}
}
println out
def list = new StringWriter()
new StreamingJsonBuilder(list).call([1, 2, 3]) { n -> value n * 2 }
println list
def file = File.createTempFile('readings', '.json')
file.withWriter('UTF-8') { writer ->
new StreamingJsonBuilder(writer).call((1..3).collect { [id: it, temperature: 20 + it] })
}
println file.text
file.delete()
def trimmed = new StringWriter()
new StreamingJsonBuilder(trimmed, new JsonGenerator.Options().excludeNulls().build()).call { a null; b 'x' }
println trimmed
{"reading":{"sensor":"bench-1","values":[21.5,22.0],"meta":{"unit":"C"}}}
[{"value":2},{"value":4},{"value":6}]
[{"id":1,"temperature":21},{"id":2,"temperature":22},{"id":3,"temperature":23}]
{"b":"x"}
Choose JsonBuilder when the document is small or needs to be inspected or changed before it is sent, and
StreamingJsonBuilder when the output goes straight to a file, a socket or an HTTP response; the third block above writes
three readings to a file that way.
Typed parsing
JSON is usually read into a class, not navigated as a map. JsonSlurper has no parseTextAs method (the YAML and TOML
slurpers do, see YAML & TOML); the documented route is Groovy’s as
coercion, which needs no dependency.
import groovy.json.JsonSlurper
enum Season { SPRING, SUMMER }
class Plant {
String name
int waterEveryHours
BigDecimal minMoisture
boolean indoor
String toString() { "Plant($name, every ${waterEveryHours}h, moisture $minMoisture, indoor $indoor)" }
}
class Greenhouse {
String greenhouse
List<Plant> plants
Season season
}
def plant = new JsonSlurper().parseText('{"name":"basil","waterEveryHours":6,"minMoisture":0.5,"indoor":true}') as Plant
println plant
println((new JsonSlurper().parseText('{"season":"SUMMER"}') as Greenhouse).season)
def greenhouse = new JsonSlurper().parse(new File('data/plants.json')) as Greenhouse
println greenhouse.greenhouse
println greenhouse.plants[0].getClass().simpleName
Plant(basil, every 6h, moisture 0.5, indoor true)
SUMMER
Backyard East
LazyMap
The first three lines work, including the conversion of "SUMMER" to an enum constant. The last line shows the limit:
generics are erased at run time, so the plants list is still a list of maps and its elements are not Plant objects. Convert
the elements yourself (doc.plants.collect { it as Plant }) or use a library that sees the generic type. A record is built the same way from a map whose keys match its components.
as also fails quietly, which is the part to be careful with.
import groovy.json.JsonSlurper
class Plant {
String name
int waterEveryHours
String toString() { "Plant($name, every ${waterEveryHours}h)" }
}
def asPlant = { String json -> new JsonSlurper().parseText(json) as Plant }
println asPlant('{"name":"basil","waterEveryHours":6}')
println asPlant('{"name":"basil","waterEveryHours":6,"colour":"green"}') // unknown key
println asPlant('{"name":"basil","waterEveryHours":"soon"}') // wrong type
println asPlant('{"name":"basil","waterEveryHours":"6"}') // numeric string
println asPlant('{"name":"basil","waterEveryHours":2.9}') // fraction
try {
new Plant(new JsonSlurper().parseText('{"name":"basil","colour":"green"}'))
} catch (MissingPropertyException e) {
println e.message
}
Plant(basil, every 6h)
Plant(null, every 0h)
Plant(null, every 0h)
Plant(basil, every 54h)
Plant(basil, every 2h)
No such property: colour for class: Plant
An unknown key (colour) or a value of the wrong type ("soon") produces an object with default values and no error. A
numeric string is converted through a character code ("6" becomes 54), and a fraction is truncated. If you want an
exception, call the constructor yourself, as the last lines do: new Plant(map) throws
MissingPropertyException for an unknown key. For strict mapping, nested generic types, dates and annotations, use Jackson
directly, as the official documentation suggests. Jackson ships in the distribution’s lib directory (the YAML and TOML
modules depend on it); a project declares com.fasterxml.jackson.core:jackson-databind itself.
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
class Plant {
String name
int waterEveryHours
BigDecimal minMoisture
boolean indoor
}
class Greenhouse {
String greenhouse
List<Plant> plants
}
def mapper = new ObjectMapper()
try {
mapper.readValue(new File('data/plants.json'), Greenhouse)
} catch (e) {
println e.getClass().simpleName
println e.message.readLines().first()
}
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
def greenhouse = mapper.readValue(new File('data/plants.json'), Greenhouse)
println greenhouse.plants*.getClass()*.simpleName.unique()
println greenhouse.plants[1].with { "$name $waterEveryHours $minMoisture $indoor" }
println mapper.writeValueAsString(greenhouse.plants[0])
UnrecognizedPropertyException
Unrecognized field "species" (class Plant), not marked as ignorable (5 known properties: "minMoisture", "name", "waterEveryHours", "indoor", "metaClass")
[Plant]
basil 6 0.5 true
{"name":"tomatoes","waterEveryHours":2,"minMoisture":0.35,"indoor":true}
Jackson rejects the species key because the class has no such property, which is the strictness as lacks, and with that
check switched off it builds a real List<Plant>. The metaClass property in the message is Groovy’s own, which Jackson
sees on every Groovy object; it is harmless here.
Source: Groovy documentation: Typed JSON parsing
Reading, changing and writing back
The pieces combine into the usual load, modify, save cycle. The default parser returns mutable collections, so no copy is needed.
import groovy.json.*
def source = new File('data/plants.json')
def data = new JsonSlurper().parse(source)
data.plants.each { it.waterEveryHours *= 2 }
data.plants << [name: 'mint', species: 'Mentha spicata', waterEveryHours: 4,
minMoisture: 0.6, indoor: true, notes: null]
def copy = File.createTempFile('plants', '.json')
copy.setText(JsonOutput.prettyPrint(JsonOutput.toJson(data)), 'UTF-8')
def again = new JsonSlurper().parse(copy)
println again.plants.collect { "${it.name}: every ${it.waterEveryHours}h" }
println again == data
println JsonOutput.toJson(again.plants*.subMap(['name', 'indoor']))
copy.delete()
[tomatoes: every 4h, basil: every 12h, chillies: every 24h, mint: every 4h]
true
[{"name":"tomatoes","indoor":true},{"name":"basil","indoor":true},{"name":"chillies","indoor":false},{"name":"mint","indoor":true}]
Quick reference
| Task | Call |
|---|---|
Parse a string |
|
Parse a file, path or reader |
|
Parse a very large file |
|
Allow comments and bare keys |
|
Keep a time-zone offset |
|
Limit hostile input |
|
Write compact JSON |
|
Write indented JSON |
|
Control fields, dates, types |
|
Build with the DSL |
|
Read into a class |
|
Where the book is outdated
The Manning book Groovy in Action (publisher page in the references) predates JSON support in the library, so its text has no equivalent of this page.
-
JSON support arrived in Groovy 1.8 (2011) and has been in the separate
groovy-jsonmodule since the 2.0 split, so the dependency is not on the core classpath of an older build. -
The parser variants, the
JsonDateHandlingoptions,JsonGeneratorand the nesting and number-length limits are all newer than the book and are documented here for the 6.0 line. -
Typed parsing in current Groovy is
ascoercion or Jackson, not a built-inparseTextAsonJsonSlurper. -
Version and JDK baselines: Groovy 6.0 needs JDK 17 or later.