Add support for "application/jsonl" JSON lines

Prior to this commit, Spring web frameworks were using the
"application/x-ndjson" media type for streaming JSON payloads delimited
with newlines.

The "application/jsonl" media type seems to gain popularity in the
broader ecosystem and could supersede NDJSON in the future. This commit
adds support for JSON Lines as an alternative.

Closes gh-36485
This commit is contained in:
Brian Clozel
2026-03-17 12:18:23 +01:00
parent e2b9b19970
commit 22e4d84993
16 changed files with 61 additions and 33 deletions
@@ -209,6 +209,19 @@ public class MediaType extends MimeType implements Serializable {
*/
public static final String APPLICATION_NDJSON_VALUE = "application/x-ndjson";
/**
* Media type for {@code application/jsonl} (JSON Lines).
* @since 7.1
* @see <a href="https://jsonlines.org/">JSON Lines</a>
*/
public static final MediaType APPLICATION_JSONL;
/**
* A String equivalent of {@link MediaType#APPLICATION_JSONL}.
* @since 7.1
*/
public static final String APPLICATION_JSONL_VALUE = "application/jsonl";
/**
* Media type for {@code application/xhtml+xml}.
*/
@@ -372,6 +385,7 @@ public class MediaType extends MimeType implements Serializable {
APPLICATION_GRAPHQL_RESPONSE = new MediaType("application", "graphql-response+json");
APPLICATION_JSON = new MediaType("application", "json");
APPLICATION_NDJSON = new MediaType("application", "x-ndjson");
APPLICATION_JSONL = new MediaType("application", "jsonl");
APPLICATION_OCTET_STREAM = new MediaType("application", "octet-stream");
APPLICATION_PDF = new MediaType("application", "pdf");
APPLICATION_PROBLEM_JSON = new MediaType("application", "problem+json");
@@ -58,7 +58,8 @@ public class GsonEncoder extends AbstractEncoder<Object> implements HttpMessageE
private static final MimeType[] DEFAULT_JSON_MIME_TYPES = new MimeType[] {
MediaType.APPLICATION_JSON,
new MediaType("application", "*+json"),
MediaType.APPLICATION_NDJSON
MediaType.APPLICATION_NDJSON,
MediaType.APPLICATION_JSONL
};
private final Gson gson;
@@ -68,11 +69,12 @@ public class GsonEncoder extends AbstractEncoder<Object> implements HttpMessageE
/**
* Construct a new encoder using a default {@link Gson} instance
* and the {@code "application/json"} and {@code "application/*+json"}
* MIME types. The {@code "application/x-ndjson"} is configured for streaming.
* MIME types. The {@code "application/jsonl"} and {@code "application/x-ndjson"}
* are configured for streaming.
*/
public GsonEncoder() {
this(new Gson(), DEFAULT_JSON_MIME_TYPES);
setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON));
setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON, MediaType.APPLICATION_JSONL));
}
/**
@@ -81,7 +81,8 @@ public abstract class Jackson2CodecSupport {
private static final List<MimeType> defaultMimeTypes = List.of(
MediaType.APPLICATION_JSON,
new MediaType("application", "*+json"),
MediaType.APPLICATION_NDJSON);
MediaType.APPLICATION_NDJSON,
MediaType.APPLICATION_JSONL);
protected final Log logger = HttpLogging.forLogName(getClass());
@@ -63,7 +63,7 @@ public class Jackson2JsonEncoder extends AbstractJackson2Encoder {
public Jackson2JsonEncoder(ObjectMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
setStreamingMediaTypes(Arrays.asList(MediaType.APPLICATION_NDJSON));
setStreamingMediaTypes(Arrays.asList(MediaType.APPLICATION_NDJSON, MediaType.APPLICATION_JSONL));
this.ssePrettyPrinter = initSsePrettyPrinter();
}
@@ -55,7 +55,8 @@ public class JacksonJsonDecoder extends AbstractJacksonDecoder<JsonMapper> {
private static final MimeType[] DEFAULT_JSON_MIME_TYPES = new MimeType[] {
MediaType.APPLICATION_JSON,
new MediaType("application", "*+json"),
MediaType.APPLICATION_NDJSON
MediaType.APPLICATION_NDJSON,
MediaType.APPLICATION_JSONL
};
@@ -55,7 +55,8 @@ public class JacksonJsonEncoder extends AbstractJacksonEncoder<JsonMapper> {
private static final MimeType[] DEFAULT_JSON_MIME_TYPES = new MimeType[] {
MediaType.APPLICATION_JSON,
new MediaType("application", "*+json"),
MediaType.APPLICATION_NDJSON
MediaType.APPLICATION_NDJSON,
MediaType.APPLICATION_JSONL
};
@@ -99,7 +100,7 @@ public class JacksonJsonEncoder extends AbstractJacksonEncoder<JsonMapper> {
*/
public JacksonJsonEncoder(JsonMapper.Builder builder, MimeType... mimeTypes) {
super(builder.addMixIn(ProblemDetail.class, ProblemDetailJacksonMixin.class), mimeTypes);
setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON));
setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON, MediaType.APPLICATION_JSONL));
this.ssePrettyPrinter = initSsePrettyPrinter();
}
@@ -110,7 +111,7 @@ public class JacksonJsonEncoder extends AbstractJacksonEncoder<JsonMapper> {
*/
public JacksonJsonEncoder(JsonMapper mapper, MimeType... mimeTypes) {
super(mapper, mimeTypes);
setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON));
setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON, MediaType.APPLICATION_JSONL));
this.ssePrettyPrinter = initSsePrettyPrinter();
}
@@ -56,7 +56,8 @@ public class KotlinSerializationJsonDecoder extends KotlinSerializationStringDec
private static final MimeType[] DEFAULT_JSON_MIME_TYPES = new MimeType[] {
MediaType.APPLICATION_JSON,
new MediaType("application", "*+json"),
MediaType.APPLICATION_NDJSON
MediaType.APPLICATION_NDJSON,
MediaType.APPLICATION_JSONL
};
/**
@@ -36,7 +36,7 @@ import org.springframework.util.MimeType;
/**
* Encode from an {@code Object} stream to a byte stream of JSON objects using
* <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
* It supports {@code application/json}, {@code application/x-ndjson} and {@code application/*+json} with
* It supports {@code application/json}, {@code application/x-ndjson}, {@code application/jsonl} and {@code application/*+json} with
* various character sets, {@code UTF-8} being the default.
*
* <p>As of Spring Framework 7.0, by default it only encodes types annotated with
@@ -59,7 +59,8 @@ public class KotlinSerializationJsonEncoder extends KotlinSerializationStringEnc
private static final MimeType[] DEFAULT_JSON_MIME_TYPES = new MimeType[] {
MediaType.APPLICATION_JSON,
new MediaType("application", "*+json"),
MediaType.APPLICATION_NDJSON
MediaType.APPLICATION_NDJSON,
MediaType.APPLICATION_JSONL
};
/**
@@ -87,7 +88,7 @@ public class KotlinSerializationJsonEncoder extends KotlinSerializationStringEnc
*/
public KotlinSerializationJsonEncoder(Json json) {
super(json, DEFAULT_JSON_MIME_TYPES);
setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON));
setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON, MediaType.APPLICATION_JSONL));
}
/**
@@ -97,7 +98,7 @@ public class KotlinSerializationJsonEncoder extends KotlinSerializationStringEnc
*/
public KotlinSerializationJsonEncoder(Json json, Predicate<ResolvableType> typePredicate) {
super(json, typePredicate, DEFAULT_JSON_MIME_TYPES);
setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON));
setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON, MediaType.APPLICATION_JSONL));
}
@Override
@@ -80,7 +80,7 @@ public class ProtobufJsonEncoder implements HttpMessageEncoder<Message> {
@Override
public List<MediaType> getStreamingMediaTypes() {
return List.of(MediaType.APPLICATION_NDJSON);
return List.of(MediaType.APPLICATION_NDJSON, MediaType.APPLICATION_JSONL);
}
@Override