Add kotlinx.serialization JSON support to Spring WebFlux
Flow decoding is not supported yet since it depends on kotlin/kotlinx.serialization#1073, but it will be enabled when this issue will be fixed. Closes gh-25771
This commit is contained in:
@@ -176,6 +176,22 @@ public interface CodecConfigurer {
|
||||
*/
|
||||
void jaxb2Encoder(Encoder<?> encoder);
|
||||
|
||||
/**
|
||||
* Override the default Kotlin Serialization JSON {@code Decoder}.
|
||||
* @param decoder the decoder instance to use
|
||||
* @since 5.3
|
||||
* @see org.springframework.http.codec.json.KotlinSerializationJsonDecoder
|
||||
*/
|
||||
void kotlinSerializationJsonDecoder(Decoder<?> decoder);
|
||||
|
||||
/**
|
||||
* Override the default Kotlin Serialization JSON {@code Encoder}.
|
||||
* @param encoder the encoder instance to use
|
||||
* @since 5.3
|
||||
* @see org.springframework.http.codec.json.KotlinSerializationJsonEncoder
|
||||
*/
|
||||
void kotlinSerializationJsonEncoder(Encoder<?> encoder);
|
||||
|
||||
/**
|
||||
* Configure a limit on the number of bytes that can be buffered whenever
|
||||
* the input stream needs to be aggregated. This can be a result of
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed 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
|
||||
*
|
||||
* https://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 org.springframework.http.codec.json;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
|
||||
import kotlinx.serialization.KSerializer;
|
||||
import kotlinx.serialization.SerializersKt;
|
||||
import kotlinx.serialization.json.Json;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.AbstractDecoder;
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
/**
|
||||
* Decode a byte stream into JSON and convert to Object's with
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
|
||||
*
|
||||
* <p>This decoder can be used to bind {@code @Serializable} Kotlin classes.
|
||||
* It supports {@code application/json} and {@code application/*+json} with
|
||||
* various character sets, {@code UTF-8} being the default.
|
||||
*
|
||||
* <p>Decoding streams is not supported yet, see
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization/issues/1073">kotlinx.serialization/issues/1073</a>
|
||||
* related issue.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.3
|
||||
*/
|
||||
public class KotlinSerializationJsonDecoder extends AbstractDecoder<Object> {
|
||||
|
||||
private static final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
private final Json json;
|
||||
|
||||
// String decoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details
|
||||
private final StringDecoder stringDecoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false);
|
||||
|
||||
public KotlinSerializationJsonDecoder() {
|
||||
this(Json.Default);
|
||||
}
|
||||
|
||||
public KotlinSerializationJsonDecoder(Json json) {
|
||||
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
|
||||
this.json = json;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canDecode(ResolvableType elementType, MimeType mimeType) {
|
||||
return super.canDecode(elementType, mimeType) && (!CharSequence.class.isAssignableFrom(elementType.toClass()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
|
||||
return Flux.error(new UnsupportedOperationException());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
|
||||
return stringDecoder
|
||||
.decodeToMono(inputStream, elementType, mimeType, hints)
|
||||
.map(jsonText -> this.json.decodeFromString(serializer(elementType.getType()), jsonText));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find a serializer that can marshall or unmarshall instances of the given type
|
||||
* using kotlinx.serialization. If no serializer can be found, an exception is thrown.
|
||||
* <p>Resolved serializers are cached and cached results are returned on successive calls.
|
||||
* @param type the type to find a serializer for
|
||||
* @return a resolved serializer for the given type
|
||||
* @throws RuntimeException if no serializer supporting the given type can be found
|
||||
*/
|
||||
private KSerializer<Object> serializer(Type type) {
|
||||
KSerializer<Object> serializer = serializerCache.get(type);
|
||||
if (serializer == null) {
|
||||
serializer = SerializersKt.serializer(type);
|
||||
serializerCache.put(type, serializer);
|
||||
}
|
||||
return serializer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed 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
|
||||
*
|
||||
* https://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 org.springframework.http.codec.json;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import kotlinx.serialization.KSerializer;
|
||||
import kotlinx.serialization.SerializersKt;
|
||||
import kotlinx.serialization.json.Json;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.AbstractEncoder;
|
||||
import org.springframework.core.codec.CharSequenceEncoder;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
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>.
|
||||
*
|
||||
* <p>This encoder can be used to bind {@code @Serializable} Kotlin classes.
|
||||
* It supports {@code application/json} and {@code application/*+json} with
|
||||
* various character sets, {@code UTF-8} being the default.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.3
|
||||
*/
|
||||
public class KotlinSerializationJsonEncoder extends AbstractEncoder<Object> {
|
||||
|
||||
private static final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
private final Json json;
|
||||
|
||||
// CharSequence encoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details
|
||||
private final CharSequenceEncoder charSequenceEncoder = CharSequenceEncoder.allMimeTypes();
|
||||
|
||||
public KotlinSerializationJsonEncoder() {
|
||||
this(Json.Default);
|
||||
}
|
||||
|
||||
public KotlinSerializationJsonEncoder(Json json) {
|
||||
super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
|
||||
this.json = json;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canEncode(ResolvableType elementType, MimeType mimeType) {
|
||||
return super.canEncode(elementType, mimeType)
|
||||
&& (!String.class.isAssignableFrom(elementType.toClass()))
|
||||
&& (!ServerSentEvent.class.isAssignableFrom(elementType.toClass()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory, ResolvableType elementType, MimeType mimeType, Map<String, Object> hints) {
|
||||
if (inputStream instanceof Mono) {
|
||||
return Mono.from(inputStream)
|
||||
.map(value -> encodeValue(value, bufferFactory, elementType, mimeType, hints))
|
||||
.flux();
|
||||
}
|
||||
else {
|
||||
ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType);
|
||||
return Flux.from(inputStream)
|
||||
.collectList()
|
||||
.map(list -> encodeValue(list, bufferFactory, listType, mimeType, hints))
|
||||
.flux();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, ResolvableType valueType, MimeType mimeType, Map<String, Object> hints) {
|
||||
String json = this.json.encodeToString(serializer(valueType.getType()), value);
|
||||
return charSequenceEncoder.encodeValue(json, bufferFactory, valueType, mimeType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find a serializer that can marshall or unmarshall instances of the given type
|
||||
* using kotlinx.serialization. If no serializer can be found, an exception is thrown.
|
||||
* <p>Resolved serializers are cached and cached results are returned on successive calls.
|
||||
* @param type the type to find a serializer for
|
||||
* @return a resolved serializer for the given type
|
||||
* @throws RuntimeException if no serializer supporting the given type can be found
|
||||
*/
|
||||
private KSerializer<Object> serializer(Type type) {
|
||||
KSerializer<Object> serializer = serializerCache.get(type);
|
||||
if (serializer == null) {
|
||||
serializer = SerializersKt.serializer(type);
|
||||
serializerCache.put(type, serializer);
|
||||
}
|
||||
return serializer;
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,8 @@ import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
import org.springframework.http.codec.json.Jackson2SmileDecoder;
|
||||
import org.springframework.http.codec.json.Jackson2SmileEncoder;
|
||||
import org.springframework.http.codec.json.KotlinSerializationJsonDecoder;
|
||||
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder;
|
||||
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
|
||||
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
|
||||
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
|
||||
@@ -91,6 +93,8 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||
|
||||
static final boolean nettyByteBufPresent;
|
||||
|
||||
static final boolean kotlinSerializationJsonPresent;
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = BaseCodecConfigurer.class.getClassLoader();
|
||||
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
|
||||
@@ -100,6 +104,7 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||
protobufPresent = ClassUtils.isPresent("com.google.protobuf.Message", classLoader);
|
||||
synchronossMultipartPresent = ClassUtils.isPresent("org.synchronoss.cloud.nio.multipart.NioMultipartParser", classLoader);
|
||||
nettyByteBufPresent = ClassUtils.isPresent("io.netty.buffer.ByteBuf", classLoader);
|
||||
kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
|
||||
}
|
||||
|
||||
|
||||
@@ -127,6 +132,12 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||
@Nullable
|
||||
private Encoder<?> jaxb2Encoder;
|
||||
|
||||
@Nullable
|
||||
private Decoder<?> kotlinSerializationJsonDecoder;
|
||||
|
||||
@Nullable
|
||||
private Encoder<?> kotlinSerializationJsonEncoder;
|
||||
|
||||
@Nullable
|
||||
private Integer maxInMemorySize;
|
||||
|
||||
@@ -151,6 +162,8 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||
this.protobufEncoder = other.protobufEncoder;
|
||||
this.jaxb2Decoder = other.jaxb2Decoder;
|
||||
this.jaxb2Encoder = other.jaxb2Encoder;
|
||||
this.kotlinSerializationJsonDecoder = other.kotlinSerializationJsonDecoder;
|
||||
this.kotlinSerializationJsonEncoder = other.kotlinSerializationJsonEncoder;
|
||||
this.maxInMemorySize = other.maxInMemorySize;
|
||||
this.enableLoggingRequestDetails = other.enableLoggingRequestDetails;
|
||||
this.registerDefaults = other.registerDefaults;
|
||||
@@ -196,6 +209,16 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||
this.jaxb2Encoder = encoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void kotlinSerializationJsonDecoder(Decoder<?> decoder) {
|
||||
this.kotlinSerializationJsonDecoder = decoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void kotlinSerializationJsonEncoder(Encoder<?> encoder) {
|
||||
this.kotlinSerializationJsonEncoder = encoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maxInMemorySize(int byteCount) {
|
||||
this.maxInMemorySize = byteCount;
|
||||
@@ -365,6 +388,9 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||
if (jackson2Present) {
|
||||
addCodec(readers, new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
|
||||
}
|
||||
else if (kotlinSerializationJsonPresent) {
|
||||
addCodec(readers, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder()));
|
||||
}
|
||||
if (jackson2SmilePresent) {
|
||||
addCodec(readers, new DecoderHttpMessageReader<>(this.jackson2SmileDecoder != null ?
|
||||
(Jackson2SmileDecoder) this.jackson2SmileDecoder : new Jackson2SmileDecoder()));
|
||||
@@ -461,6 +487,9 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||
if (jackson2Present) {
|
||||
writers.add(new EncoderHttpMessageWriter<>(getJackson2JsonEncoder()));
|
||||
}
|
||||
else if (kotlinSerializationJsonPresent) {
|
||||
writers.add(new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder()));
|
||||
}
|
||||
if (jackson2SmilePresent) {
|
||||
writers.add(new EncoderHttpMessageWriter<>(this.jackson2SmileEncoder != null ?
|
||||
(Jackson2SmileEncoder) this.jackson2SmileEncoder : new Jackson2SmileEncoder()));
|
||||
@@ -522,4 +551,18 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||
return this.jackson2JsonEncoder;
|
||||
}
|
||||
|
||||
protected Decoder<?> getKotlinSerializationJsonDecoder() {
|
||||
if (this.kotlinSerializationJsonDecoder == null) {
|
||||
this.kotlinSerializationJsonDecoder = new KotlinSerializationJsonDecoder();
|
||||
}
|
||||
return this.kotlinSerializationJsonDecoder;
|
||||
}
|
||||
|
||||
protected Encoder<?> getKotlinSerializationJsonEncoder() {
|
||||
if (this.kotlinSerializationJsonEncoder == null) {
|
||||
this.kotlinSerializationJsonEncoder = new KotlinSerializationJsonEncoder();
|
||||
}
|
||||
return this.kotlinSerializationJsonEncoder;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -96,9 +96,10 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
|
||||
@Override
|
||||
protected void extendObjectReaders(List<HttpMessageReader<?>> objectReaders) {
|
||||
|
||||
Decoder<?> decoder = (this.sseDecoder != null ?
|
||||
this.sseDecoder :
|
||||
jackson2Present ? getJackson2JsonDecoder() : null);
|
||||
Decoder<?> decoder = (this.sseDecoder != null ? this.sseDecoder :
|
||||
jackson2Present ? getJackson2JsonDecoder() :
|
||||
kotlinSerializationJsonPresent ? getKotlinSerializationJsonDecoder() :
|
||||
null);
|
||||
|
||||
addCodec(objectReaders, new ServerSentEventHttpMessageReader(decoder));
|
||||
}
|
||||
|
||||
@@ -86,7 +86,10 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo
|
||||
|
||||
@Nullable
|
||||
private Encoder<?> getSseEncoder() {
|
||||
return this.sseEncoder != null ? this.sseEncoder : jackson2Present ? getJackson2JsonEncoder() : null;
|
||||
return this.sseEncoder != null ? this.sseEncoder :
|
||||
jackson2Present ? getJackson2JsonEncoder() :
|
||||
kotlinSerializationJsonPresent ? getKotlinSerializationJsonEncoder() :
|
||||
null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed 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
|
||||
*
|
||||
* https://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 org.springframework.http.codec.json
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.core.ResolvableType
|
||||
import org.springframework.core.io.buffer.DataBuffer
|
||||
import org.springframework.core.testfixture.codec.AbstractDecoderTests
|
||||
import org.springframework.http.MediaType
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.test.StepVerifier
|
||||
import reactor.test.StepVerifier.FirstStep
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* Tests for the JSON decoding using kotlinx.serialization.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class KotlinSerializationJsonDecoderTests : AbstractDecoderTests<KotlinSerializationJsonDecoder>(KotlinSerializationJsonDecoder()) {
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax", "DEPRECATION")
|
||||
@Test
|
||||
override fun canDecode() {
|
||||
val jsonSubtype = MediaType("application", "vnd.test-micro-type+json")
|
||||
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java), MediaType.APPLICATION_JSON)).isTrue()
|
||||
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java), jsonSubtype)).isTrue()
|
||||
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java), null)).isTrue()
|
||||
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(String::class.java), null)).isFalse()
|
||||
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java), MediaType.APPLICATION_XML)).isFalse()
|
||||
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java),
|
||||
MediaType("application", "json", StandardCharsets.UTF_8))).isTrue()
|
||||
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java),
|
||||
MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue()
|
||||
Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java),
|
||||
MediaType("application", "json", StandardCharsets.ISO_8859_1))).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun decode() {
|
||||
val output = decoder.decode(Mono.empty(), ResolvableType.forClass(Pojo::class.java), null, emptyMap())
|
||||
StepVerifier
|
||||
.create(output)
|
||||
.expectError(UnsupportedOperationException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun decodeToMono() {
|
||||
val input = Flux.concat(
|
||||
stringBuffer("[{\"bar\":\"b1\",\"foo\":\"f1\"},"),
|
||||
stringBuffer("{\"bar\":\"b2\",\"foo\":\"f2\"}]"))
|
||||
|
||||
val elementType = ResolvableType.forClassWithGenerics(List::class.java, Pojo::class.java)
|
||||
|
||||
testDecodeToMonoAll(input, elementType, { step: FirstStep<Any> ->
|
||||
step
|
||||
.expectNext(listOf(Pojo("f1", "b1"), Pojo("f2", "b2")))
|
||||
.expectComplete()
|
||||
.verify()
|
||||
}, null, null)
|
||||
}
|
||||
|
||||
private fun stringBuffer(value: String): Mono<DataBuffer> {
|
||||
return stringBuffer(value, StandardCharsets.UTF_8)
|
||||
}
|
||||
|
||||
private fun stringBuffer(value: String, charset: Charset): Mono<DataBuffer> {
|
||||
return Mono.defer {
|
||||
val bytes = value.toByteArray(charset)
|
||||
val buffer = bufferFactory.allocateBuffer(bytes.size)
|
||||
buffer.write(bytes)
|
||||
Mono.just(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Pojo(val foo: String, val bar: String)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed 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
|
||||
*
|
||||
* https://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 org.springframework.http.codec.json
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.core.ResolvableType
|
||||
import org.springframework.core.io.buffer.DataBuffer
|
||||
import org.springframework.core.io.buffer.DataBufferUtils
|
||||
import org.springframework.core.testfixture.codec.AbstractEncoderTests
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.codec.ServerSentEvent
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import reactor.test.StepVerifier.FirstStep
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* Tests for the JSON encoding using kotlinx.serialization.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
class KotlinSerializationJsonEncoderTests : AbstractEncoderTests<KotlinSerializationJsonEncoder>(KotlinSerializationJsonEncoder()) {
|
||||
|
||||
@Test
|
||||
override fun canEncode() {
|
||||
val pojoType = ResolvableType.forClass(Pojo::class.java)
|
||||
val jsonSubtype = MediaType("application", "vnd.test-micro-type+json")
|
||||
Assertions.assertThat(encoder.canEncode(pojoType, MediaType.APPLICATION_JSON)).isTrue()
|
||||
Assertions.assertThat(encoder.canEncode(pojoType, jsonSubtype)).isTrue()
|
||||
Assertions.assertThat(encoder.canEncode(pojoType, null)).isTrue()
|
||||
Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(Pojo::class.java),
|
||||
MediaType("application", "json", StandardCharsets.UTF_8))).isTrue()
|
||||
Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(Pojo::class.java),
|
||||
MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue()
|
||||
Assertions.assertThat(encoder.canEncode(ResolvableType.NONE, null)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun encode() {
|
||||
val input = Flux.just(
|
||||
Pojo("foo", "bar"),
|
||||
Pojo("foofoo", "barbar"),
|
||||
Pojo("foofoofoo", "barbarbar")
|
||||
)
|
||||
testEncode(input, Pojo::class.java, { step: FirstStep<DataBuffer?> ->
|
||||
step
|
||||
.consumeNextWith(expectString("[" +
|
||||
"{\"foo\":\"foo\",\"bar\":\"bar\"}," +
|
||||
"{\"foo\":\"foofoo\",\"bar\":\"barbar\"}," +
|
||||
"{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}]")
|
||||
.andThen { dataBuffer: DataBuffer? -> DataBufferUtils.release(dataBuffer) })
|
||||
.verifyComplete()
|
||||
})
|
||||
}
|
||||
|
||||
@Test
|
||||
fun encodeMono() {
|
||||
val input = Mono.just(Pojo("foo", "bar"))
|
||||
testEncode(input, Pojo::class.java, { step: FirstStep<DataBuffer?> ->
|
||||
step
|
||||
.consumeNextWith(expectString("{\"foo\":\"foo\",\"bar\":\"bar\"}")
|
||||
.andThen { dataBuffer: DataBuffer? -> DataBufferUtils.release(dataBuffer) })
|
||||
.verifyComplete()
|
||||
})
|
||||
}
|
||||
|
||||
@Test
|
||||
fun canNotEncode() {
|
||||
Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(String::class.java), null)).isFalse()
|
||||
Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(Pojo::class.java), MediaType.APPLICATION_XML)).isFalse()
|
||||
val sseType = ResolvableType.forClass(ServerSentEvent::class.java)
|
||||
Assertions.assertThat(encoder.canEncode(sseType, MediaType.APPLICATION_JSON)).isFalse()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Pojo(val foo: String, val bar: String)
|
||||
|
||||
}
|
||||
@@ -389,14 +389,14 @@ project for more details.
|
||||
=== Kotlin multiplatform serialization
|
||||
|
||||
As of Spring Framework 5.3, https://github.com/Kotlin/kotlinx.serialization[Kotlin multiplatform serialization] is
|
||||
supported in Spring MVC. The builtin support currently only targets JSON format.
|
||||
supported in Spring MVC and Spring WebFlux. The builtin support currently only targets JSON format.
|
||||
|
||||
To enable it, follow https://github.com/Kotlin/kotlinx.serialization#setup[those instructions] and make sure neither
|
||||
Jackson, GSON or JSONB are in the classpath.
|
||||
|
||||
NOTE: For a typical Spring Boot web application, that can be achieved by excluding `spring-boot-starter-json` dependency.
|
||||
|
||||
If you need Jackson, GSON or JSONB for other purposes, you can keep them on the classpath and
|
||||
In Spring MVC, if you need Jackson, GSON or JSONB for other purposes, you can keep them on the classpath and
|
||||
<<web#mvc-config-message-converters, configure message converters>> to remove `MappingJackson2HttpMessageConverter` and add
|
||||
`KotlinSerializationJsonHttpMessageConverter`.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user