JsonValueWriter Class — spring-boot Architecture
Architecture documentation for the JsonValueWriter class in JsonValueWriter.java from the spring-boot codebase.
Entity Profile
Relationship Graph
Source Code
core/spring-boot/src/main/java/org/springframework/boot/json/JsonValueWriter.java lines 50–426
class JsonValueWriter {
private static final int DEFAULT_MAX_NESTING_DEPTH = 500;
private final Appendable out;
private final int maxNestingDepth;
private MemberPath path = MemberPath.ROOT;
private final Deque<JsonWriterFiltersAndProcessors> filtersAndProcessors = new ArrayDeque<>();
private final Deque<ActiveSeries> activeSeries = new ArrayDeque<>();
/**
* Create a new {@link JsonValueWriter} instance.
* @param out the {@link Appendable} used to receive the JSON output
*/
JsonValueWriter(Appendable out) {
this(out, DEFAULT_MAX_NESTING_DEPTH);
}
/**
* Create a new {@link JsonValueWriter} instance.
* @param out the {@link Appendable} used to receive the JSON output
* @param maxNestingDepth the maximum allowed nesting depth for JSON objects and
* arrays
*/
JsonValueWriter(Appendable out, int maxNestingDepth) {
this.out = out;
this.maxNestingDepth = maxNestingDepth;
}
void pushProcessors(JsonWriterFiltersAndProcessors jsonProcessors) {
this.filtersAndProcessors.addLast(jsonProcessors);
}
void popProcessors() {
this.filtersAndProcessors.removeLast();
}
/**
* Write a name value pair, or just a value if {@code name} is {@code null}.
* @param <N> the name type in the pair
* @param <V> the value type in the pair
* @param name the name of the pair or {@code null} if only the value should be
* written
* @param value the value
*/
<N, V> void write(@Nullable N name, @Nullable V value) {
if (name != null) {
writePair(name, value);
}
else {
write(value);
}
}
/**
* Write a value to the JSON output. The following value types are supported:
* <ul>
* <li>Any {@code null} value</li>
* <li>A {@link WritableJson} instance</li>
* <li>Any {@link Iterable} or Array (written as a JSON array)</li>
* <li>A {@link Map} (written as a JSON object)</li>
* <li>Any {@link Number}</li>
* <li>A {@link Boolean}</li>
* </ul>
* All other values are written as JSON strings.
* @param <V> the value type
* @param value the value to write
*/
<V> void write(@Nullable V value) {
value = processValue(value);
if (value == null) {
append("null");
}
else if (value instanceof WritableJson writableJson) {
try {
writableJson.to(this.out);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
else if (value instanceof Iterable<?> iterable && canWriteAsArray(iterable)) {
writeArray(iterable::forEach);
}
else if (ObjectUtils.isArray(value)) {
writeArray(Arrays.asList(ObjectUtils.toObjectArray(value))::forEach);
}
else if (value instanceof Map<?, ?> map) {
writeObject(map::forEach);
}
else if (value instanceof Number || value instanceof Boolean) {
append(value.toString());
}
else {
writeString(value);
}
}
private boolean canWriteAsArray(Iterable<?> iterable) {
return !(iterable instanceof Path);
}
/**
* Start a new {@link Series} (JSON object or array).
* @param series the series to start
* @see #end(Series)
* @see #writePairs(Consumer)
* @see #writeElements(Consumer)
*/
void start(@Nullable Series series) {
if (series != null) {
int nestingDepth = this.activeSeries.size();
Assert.state(nestingDepth <= this.maxNestingDepth,
() -> "JSON nesting depth (%s) exceeds maximum depth of %s (current path: %s)"
.formatted(nestingDepth, this.maxNestingDepth, this.path));
this.activeSeries.push(new ActiveSeries(series));
append(series.openChar);
}
}
/**
* End an active {@link Series} (JSON object or array).
* @param series the series type being ended (must match {@link #start(Series)})
* @see #start(Series)
*/
void end(@Nullable Series series) {
if (series != null) {
this.activeSeries.pop();
append(series.closeChar);
}
}
/**
* Write the specified elements to a newly started {@link Series#ARRAY array series}.
* @param <E> the element type
* @param elements a callback that will be used to provide each element. Typically a
* {@code forEach} method reference.
* @see #writeElements(Consumer)
*/
<E> void writeArray(Consumer<Consumer<E>> elements) {
start(Series.ARRAY);
elements.accept(ThrowingConsumer.of(this::writeElement));
end(Series.ARRAY);
}
/**
* Write the specified elements to an already started {@link Series#ARRAY array
* series}.
* @param <E> the element type
* @param elements a callback that will be used to provide each element. Typically a
* {@code forEach} method reference.
* @see #writeElements(Consumer)
*/
<E> void writeElements(Consumer<Consumer<E>> elements) {
elements.accept(ThrowingConsumer.of(this::writeElement));
}
<E> void writeElement(E element) {
ActiveSeries activeSeries = this.activeSeries.peek();
Assert.state(activeSeries != null, "No series has been started");
this.path = activeSeries.updatePath(this.path);
activeSeries.incrementIndexAndAddCommaIfRequired();
write(element);
this.path = activeSeries.restorePath(this.path);
}
/**
* Write the specified pairs to a newly started {@link Series#OBJECT object series}.
* @param <N> the name type in the pair
* @param <V> the value type in the pair
* @param pairs a callback that will be used to provide each pair. Typically a
* {@code forEach} method reference.
* @see #writePairs(Consumer)
*/
<N, V> void writeObject(Consumer<BiConsumer<N, V>> pairs) {
start(Series.OBJECT);
pairs.accept(this::writePair);
end(Series.OBJECT);
}
/**
* Write the specified pairs to an already started {@link Series#OBJECT object
* series}.
* @param <N> the name type in the pair
* @param <V> the value type in the pair
* @param pairs a callback that will be used to provide each pair. Typically a
* {@code forEach} method reference.
* @see #writePairs(Consumer)
*/
<N, V> void writePairs(Consumer<BiConsumer<N, V>> pairs) {
pairs.accept(this::writePair);
}
private <N, V> void writePair(N name, @Nullable V value) {
this.path = this.path.child(name.toString());
if (!isFilteredPath()) {
String processedName = processName(name.toString());
ActiveSeries activeSeries = this.activeSeries.peek();
Assert.state(activeSeries != null, "No series has been started");
activeSeries.incrementIndexAndAddCommaIfRequired();
Assert.state(activeSeries.addName(processedName),
() -> "The name '" + processedName + "' has already been written");
writeString(processedName);
append(":");
write(value);
}
this.path = (this.path.parent() != null) ? this.path.parent() : MemberPath.ROOT;
}
private void writeString(Object value) {
try {
this.out.append('"');
String string = value.toString();
for (int i = 0; i < string.length(); i++) {
char ch = string.charAt(i);
switch (ch) {
case '"' -> this.out.append("\\\"");
case '\\' -> this.out.append("\\\\");
case '\b' -> this.out.append("\\b");
case '\f' -> this.out.append("\\f");
case '\n' -> this.out.append("\\n");
case '\r' -> this.out.append("\\r");
case '\t' -> this.out.append("\\t");
default -> {
if (Character.isISOControl(ch)) {
this.out.append("\\u");
this.out.append(String.format("%04X", (int) ch));
}
else {
this.out.append(ch);
}
}
}
}
this.out.append('"');
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private void append(String value) {
try {
this.out.append(value);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private void append(char ch) {
try {
this.out.append(ch);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private boolean isFilteredPath() {
for (JsonWriterFiltersAndProcessors filtersAndProcessors : this.filtersAndProcessors) {
for (Predicate<MemberPath> pathFilter : filtersAndProcessors.pathFilters()) {
if (pathFilter.test(this.path)) {
return true;
}
}
}
return false;
}
private String processName(String name) {
for (JsonWriterFiltersAndProcessors filtersAndProcessors : this.filtersAndProcessors) {
for (NameProcessor nameProcessor : filtersAndProcessors.nameProcessors()) {
name = processName(name, nameProcessor);
}
}
return name;
}
private String processName(String name, NameProcessor nameProcessor) {
name = nameProcessor.processName(this.path, name);
Assert.state(StringUtils.hasLength(name), "NameProcessor " + nameProcessor + " returned an empty result");
return name;
}
private <V> @Nullable V processValue(@Nullable V value) {
for (JsonWriterFiltersAndProcessors filtersAndProcessors : this.filtersAndProcessors) {
for (ValueProcessor<?> valueProcessor : filtersAndProcessors.valueProcessors()) {
value = processValue(value, valueProcessor);
}
}
return value;
}
// Lambda isn't detected with the correct nullability
@SuppressWarnings({ "unchecked", "NullAway" })
private <V> @Nullable V processValue(@Nullable V value, ValueProcessor<?> valueProcessor) {
return (V) LambdaSafe
.callback(ValueProcessor.class, valueProcessor, this.path, new @Nullable Object[] { value })
.invokeAnd((call) -> call.processValue(this.path, value))
.get(value);
}
/**
* A series of items that can be written to the JSON output.
*/
enum Series {
/**
* A JSON object series consisting of name/value pairs.
*/
OBJECT('{', '}'),
/**
* A JSON array series consisting of elements.
*/
ARRAY('[', ']');
final char openChar;
final char closeChar;
Series(char openChar, char closeChar) {
this.openChar = openChar;
this.closeChar = closeChar;
}
}
/**
* Details of the currently active {@link Series}.
*/
private final class ActiveSeries {
private final Series series;
private int index;
private final Set<String> names = new HashSet<>();
private ActiveSeries(Series series) {
this.series = series;
}
boolean addName(String processedName) {
return this.names.add(processedName);
}
MemberPath updatePath(MemberPath path) {
if (this.series != Series.ARRAY) {
return path;
}
return path.child(this.index);
}
MemberPath restorePath(MemberPath path) {
if (this.series != Series.ARRAY) {
return path;
}
return (path.parent() != null) ? path.parent() : MemberPath.ROOT;
}
void incrementIndexAndAddCommaIfRequired() {
if (this.index > 0) {
append(',');
}
this.index++;
}
}
}
Domain
Source
Analyze Your Own Codebase
Get architecture documentation, dependency graphs, and domain analysis for your codebase in minutes.
Try Supermodel Free