Skip to content

Commit

Permalink
Merge pull request #38064 from marko-bekhta/fix/i36558-kotlin-seriali…
Browse files Browse the repository at this point in the history
…zation-validation-report

Add custom Kotlin serializers for ValidationReport and Violation
  • Loading branch information
geoand authored Jan 9, 2024
2 parents fca79fe + 6b6cb77 commit 49e95bb
Show file tree
Hide file tree
Showing 12 changed files with 398 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@
import jakarta.ws.rs.core.MediaType;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.resteasy.reactive.common.deployment.ServerDefaultProducesHandlerBuildItem;
import io.quarkus.resteasy.reactive.kotlin.serialization.runtime.KotlinSerializationMessageBodyReader;
import io.quarkus.resteasy.reactive.kotlin.serialization.runtime.KotlinSerializationMessageBodyWriter;
import io.quarkus.resteasy.reactive.kotlin.serialization.runtime.ValidationJsonBuilderCustomizer;
import io.quarkus.resteasy.reactive.spi.MessageBodyReaderBuildItem;
import io.quarkus.resteasy.reactive.spi.MessageBodyWriterBuildItem;

Expand All @@ -25,11 +28,15 @@ public class KotlinSerializationProcessor {
public void additionalProviders(
BuildProducer<AdditionalBeanBuildItem> additionalBean,
BuildProducer<MessageBodyReaderBuildItem> additionalReaders,
BuildProducer<MessageBodyWriterBuildItem> additionalWriters) {
additionalBean.produce(AdditionalBeanBuildItem.builder()
.addBeanClass(KotlinSerializationMessageBodyReader.class.getName())
.addBeanClass(KotlinSerializationMessageBodyWriter.class.getName())
.setUnremovable().build());
BuildProducer<MessageBodyWriterBuildItem> additionalWriters,
Capabilities capabilities) {
AdditionalBeanBuildItem.Builder builder = AdditionalBeanBuildItem.builder()
.addBeanClasses(KotlinSerializationMessageBodyReader.class.getName(),
KotlinSerializationMessageBodyWriter.class.getName());
if (capabilities.isPresent(Capability.HIBERNATE_VALIDATOR)) {
builder.addBeanClass(ValidationJsonBuilderCustomizer.class.getName());
}
additionalBean.produce(builder.setUnremovable().build());
additionalReaders.produce(new MessageBodyReaderBuildItem(
KotlinSerializationMessageBodyReader.class.getName(), Object.class.getName(), List.of(
MediaType.APPLICATION_JSON),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class KotlinSerializationMessageBodyWriter(private val json: Json) :
if (o is String) { // YUK: done in order to avoid adding extra quotes...
entityStream.write(o.toByteArray(StandardCharsets.UTF_8))
} else {
json.encodeToStream(serializer(genericType), o, entityStream)
json.encodeToStream(json.serializersModule.serializer(genericType), o, entityStream)
}
}

Expand All @@ -42,7 +42,7 @@ class KotlinSerializationMessageBodyWriter(private val json: Json) :
if (o is String) { // YUK: done in order to avoid adding extra quotes...
stream.write(o.toByteArray(StandardCharsets.UTF_8))
} else {
json.encodeToStream(serializer(genericType), o, stream)
json.encodeToStream(json.serializersModule.serializer(genericType), o, stream)
}
// we don't use try-with-resources because that results in writing to the http output
// without the exception mapping coming into play
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.resteasy.reactive.kotlin.serialization.runtime

import io.quarkus.resteasy.reactive.kotlin.serialization.common.JsonBuilderCustomizer
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.JsonBuilder
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import kotlinx.serialization.modules.plus

class ValidationJsonBuilderCustomizer : JsonBuilderCustomizer {
@ExperimentalSerializationApi
override fun customize(jsonBuilder: JsonBuilder) {
jsonBuilder.serializersModule =
jsonBuilder.serializersModule.plus(
SerializersModule {
contextual(ViolationReportSerializer)
contextual(ViolationReportViolationSerializer)
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.quarkus.resteasy.reactive.kotlin.serialization.runtime

import io.quarkus.hibernate.validator.runtime.jaxrs.ViolationReport
import jakarta.ws.rs.core.Response
import kotlinx.serialization.*
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.listSerialDescriptor
import kotlinx.serialization.descriptors.serialDescriptor
import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure

@OptIn(ExperimentalSerializationApi::class)
@Serializer(forClass = ViolationReport::class)
object ViolationReportSerializer : KSerializer<ViolationReport> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("io.quarkus.hibernate.validator.runtime.jaxrs.ViolationReport") {
element("title", serialDescriptor<String>())
element("status", serialDescriptor<Int>())
element(
"violations",
listSerialDescriptor(ListSerializer(ViolationReportViolationSerializer).descriptor)
)
}

override fun deserialize(decoder: Decoder): ViolationReport {
return decoder.decodeStructure(descriptor) {
var title: String? = null
var status: Int? = null
var violations: List<ViolationReport.Violation> = emptyList()

loop@ while (true) {
when (val index = decodeElementIndex(descriptor)) {
DECODE_DONE -> break@loop
0 -> title = decodeStringElement(descriptor, 0)
1 -> status = decodeIntElement(descriptor, 1)
2 ->
violations =
decodeSerializableElement(
descriptor,
2,
ListSerializer(ViolationReportViolationSerializer)
)
else -> throw SerializationException("Unexpected index $index")
}
}

ViolationReport(
requireNotNull(title),
status?.let { Response.Status.fromStatusCode(it) },
violations
)
}
}

override fun serialize(encoder: Encoder, value: ViolationReport) {
encoder.encodeStructure(descriptor) {
encodeStringElement(descriptor, 0, value.title)
encodeIntElement(descriptor, 1, value.status)
encodeSerializableElement(
descriptor,
2,
ListSerializer(ViolationReportViolationSerializer),
value.violations
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.quarkus.resteasy.reactive.kotlin.serialization.runtime

import io.quarkus.hibernate.validator.runtime.jaxrs.ViolationReport
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.Serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.serialDescriptor
import kotlinx.serialization.encoding.*

@OptIn(ExperimentalSerializationApi::class)
@Serializer(forClass = ViolationReport.Violation::class)
object ViolationReportViolationSerializer : KSerializer<ViolationReport.Violation> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor(
"io.quarkus.hibernate.validator.runtime.jaxrs.ViolationReport.Violation"
) {
element("field", serialDescriptor<String>())
element("message", serialDescriptor<String>())
}

override fun deserialize(decoder: Decoder): ViolationReport.Violation {
return decoder.decodeStructure(descriptor) {
var field: String? = null
var message: String? = null

loop@ while (true) {
when (val index = decodeElementIndex(descriptor)) {
CompositeDecoder.DECODE_DONE -> break@loop
0 -> field = decodeStringElement(descriptor, 0)
1 -> message = decodeStringElement(descriptor, 1)
else -> throw SerializationException("Unexpected index $index")
}
}

ViolationReport.Violation(
requireNotNull(field),
requireNotNull(message),
)
}
}

override fun serialize(encoder: Encoder, value: ViolationReport.Violation) {
encoder.encodeStructure(descriptor) {
encodeStringElement(descriptor, 0, value.field)
encodeStringElement(descriptor, 1, value.message)
}
}
}
1 change: 1 addition & 0 deletions integration-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@
<module>rest-client-reactive</module>
<module>rest-client-reactive-http2</module>
<module>rest-client-reactive-kotlin-serialization</module>
<module>rest-client-reactive-kotlin-serialization-with-validator</module>
<module>rest-client-reactive-multipart</module>
<module>rest-client-reactive-stork</module>
<module>packaging</module>
Expand Down
Loading

0 comments on commit 49e95bb

Please sign in to comment.