diff --git a/docs-website/topics/serialization-transform-json.md b/docs-website/topics/serialization-transform-json.md
new file mode 100644
index 000000000..d671a67bb
--- /dev/null
+++ b/docs-website/topics/serialization-transform-json.md
@@ -0,0 +1,513 @@
+[//]: # (title: Transform JSON structure)
+
+
+> This section builds on concepts from [Serialize polymorphic classes](serialization-polymorphism.md) and [Create custom serializers](create-custom-serializers.md).
+> If you're not familiar with these concepts, we recommend reviewing them first.
+>
+{style="note"}
+
+To control the structure and content of the JSON you generate during serialization, you can create [custom serializers](create-custom-serializers.md).
+For smaller adjustments, the [`JsonTransformingSerializer`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-transforming-serializer/) class offers a simpler way to modify JSON by working directly with the JSON element tree instead of interacting with [`Encoder`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-encoder/)
+or [`Decoder`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx.serialization-core/kotlinx.serialization.encoding/-decoder/) manually.
+
+The `JsonTransformingSerializer` implements the [`KSerializer`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-k-serializer/) interface, which
+lets you override the [`transformSerialize()`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-transforming-serializer/transform-serialize.html) and [`transformDeserialize()`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-transforming-serializer/transform-deserialize.html) functions to adjust the JSON element tree either before serialization or before deserialization.
+
+> In addition to transforming JSON structures, you can also use `JsonContentPolymorphicSerializer` to [select the appropriate polymorphic class based on the JSON content](#select-the-appropriate-polymorphic-class-based-on-the-json-content).
+>
+{style="tip"}
+
+## Modify JSON structure
+
+You can make small structural adjustments to JSON by transforming the JSON element tree.
+The following examples demonstrate common use cases, such as wrapping or unwrapping arrays and omitting specific properties.
+
+### Wrap a single object in an array during deserialization
+
+Some APIs return a single JSON object when there is one item and an array when there are multiple.
+If you want both cases to deserialize into a [`List`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/-list/), use
+`JsonTransformingSerializer` to modify the JSON element tree during deserialization.
+To implement the wrapping logic, override the `transformDeserialize()` function.
+
+> You must specify a serializer when calling the `JsonTransformingSerializer` constructor.
+> To use the standard conversion logic for the type, specify a default serializer.
+> For example, you can use the default list serializer with the [`ListSerializer()`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.builtins/-list-serializer.html) function.
+>
+{style="note"}
+
+Here's an example:
+
+```kotlin
+// Imports declarations from the serialization library
+import kotlinx.serialization.*
+import kotlinx.serialization.builtins.*
+import kotlinx.serialization.json.*
+
+//sampleStart
+// Uses UserListSerializer to handle the serialization of the users property
+@Serializable
+data class Project(
+ val name: String,
+ // Specifies a custom serializer for the users property
+ @Serializable(with = UserListSerializer::class)
+ val users: List
+)
+
+@Serializable
+data class User(val name: String)
+
+// Specifies the default List serializer
+object UserListSerializer : JsonTransformingSerializer>(ListSerializer(User.serializer())) {
+ // If not an array, wrap the element as a single-item array
+ override fun transformDeserialize(element: JsonElement): JsonElement =
+ if (element !is JsonArray) JsonArray(listOf(element)) else element
+}
+
+fun main() {
+ // Deserializes a single JSON object wrapped as an array
+ println(Json.decodeFromString("""
+ {"name":"kotlinx.serialization","users":{"name":"kotlin"}}
+ """))
+ // Project(name=kotlinx.serialization, users=[User(name=kotlin)])
+
+ // Deserializes a JSON array of objects
+ println(Json.decodeFromString("""
+ {"name":"kotlinx.serialization","users":[{"name":"kotlin"},{"name":"jetbrains"}]}
+ """))
+ // Project(name=kotlinx.serialization, users=[User(name=kotlin), User(name=jetbrains)])
+}
+//sampleEnd
+```
+{kotlin-runnable="true"}
+
+### Unwrap a single-element array during serialization
+
+To unwrap a single-element list into a single JSON object during serialization, override the `transformSerialize()` function:
+
+```kotlin
+// Imports declarations from the serialization library
+import kotlinx.serialization.*
+import kotlinx.serialization.builtins.*
+import kotlinx.serialization.json.*
+
+//sampleStart
+@Serializable
+data class Project(
+ val name: String,
+ @Serializable(with = UserListSerializer::class)
+ val users: List
+)
+
+@Serializable
+data class User(val name: String)
+
+// Unwraps single-element lists into a single object during serialization
+object UserListSerializer : JsonTransformingSerializer>(ListSerializer(User.serializer())) {
+
+ override fun transformSerialize(element: JsonElement): JsonElement {
+ // Ensures that the input is a list
+ require(element is JsonArray)
+ // Unwraps single-element lists into a single JSON object
+ return element.singleOrNull() ?: element
+ }
+}
+
+fun main() {
+ val data = Project("kotlinx.serialization", listOf(User("kotlin")))
+ println(Json.encodeToString(data))
+ // {"name":"kotlinx.serialization","users":{"name":"kotlin"}}
+}
+//sampleEnd
+```
+{kotlin-runnable="true"}
+
+### Omit specific properties during serialization
+
+You can omit properties from the JSON output. This can be useful when they have default values, match specific values, or when the values are missing.
+This helps streamline the data, reducing unnecessary information while ensuring that only relevant properties are serialized.
+
+To omit properties during serialization, create a custom serializer and override the `transformSerialize()` function to remove the fields you don't want in the JSON output:
+
+```kotlin
+// Imports declarations from the serialization library
+import kotlinx.serialization.*
+import kotlinx.serialization.builtins.*
+import kotlinx.serialization.json.*
+
+//sampleStart
+@Serializable
+class Project(val name: String, val language: String)
+
+// Creates a custom serializer that omits the language property if it's equal to "Kotlin"
+object ProjectSerializer : JsonTransformingSerializer(Project.serializer()) {
+ override fun transformSerialize(element: JsonElement): JsonElement =
+ // Omits the language property if its value is "Kotlin"
+ JsonObject(element.jsonObject.filterNot {
+ (k, v) -> k == "language" && v.jsonPrimitive.content == "Kotlin"
+ })
+}
+
+fun main() {
+ val data = Project("kotlinx.serialization", "Kotlin")
+
+ // Uses the default serializer
+ println(Json.encodeToString(data))
+ // {"name":"kotlinx.serialization","language":"Kotlin"}
+
+ // Applies the custom serializer to omit the language property
+ println(Json.encodeToString(ProjectSerializer, data))
+ // {"name":"kotlinx.serialization"}
+}
+//sampleEnd
+```
+{kotlin-runnable="true"}
+
+In this example, the `Project` class has a `language` property, which is omitted when its value is `"Kotlin"`.
+The custom serializer is passed to `encodeToString()` to ensure the custom transformation is applied.
+
+> When serializing an object directly, you need to explicitly pass the custom serializer to the `encodeToString()`
+> function to ensure that the custom serialization logic is applied. For more information, see the [Pass serializers manually](third-party-classes.md#pass-serializers-manually) section.
+>
+{style="note"}
+
+## Select the appropriate polymorphic class based on the JSON content
+
+In [polymorphic serialization](serialization-polymorphism.md), JSON often includes a dedicated _class discriminator_ property that identifies the concrete subtype during deserialization.
+
+When no class discriminator is present in the JSON input, you can use [`JsonContentPolymorphicSerializer`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-content-polymorphic-serializer/) to infer the type from the structure of the JSON.
+This serializer allows you to override the `selectDeserializer()` function to choose the correct deserializer based on the JSON content.
+
+> When you use this serializer, the appropriate deserializer is chosen at runtime.
+> It may be one you [specified in a `SerializersModule`](serialization-polymorphism.md#serialize-closed-polymorphic-classes) or the default serializer.
+>
+{style="tip"}
+
+Here's an example:
+
+```kotlin
+// Imports declarations from the serialization library
+import kotlinx.serialization.*
+import kotlinx.serialization.builtins.*
+import kotlinx.serialization.json.*
+
+//sampleStart
+@Serializable
+abstract class Project {
+ abstract val name: String
+}
+
+@Serializable
+data class BasicProject(override val name: String): Project()
+
+@Serializable
+data class OwnedProject(override val name: String, val owner: String) : Project()
+
+// Creates a custom serializer that selects deserializer based on the presence of "owner"
+object ProjectSerializer : JsonContentPolymorphicSerializer(Project::class) {
+ override fun selectDeserializer(element: JsonElement) = when {
+ // Selects the OwnedProject serializer if the JSON object contains an "owner" key
+ "owner" in element.jsonObject -> OwnedProject.serializer()
+ else -> BasicProject.serializer()
+ }
+}
+
+fun main() {
+ val data = listOf(
+ OwnedProject("kotlinx.serialization", "kotlin"),
+ BasicProject("example")
+ )
+ // No class discriminator in the JSON output
+ val string = Json.encodeToString(ListSerializer(ProjectSerializer), data)
+
+ println(string)
+ // [{"name":"kotlinx.serialization","owner":"kotlin"},{"name":"example"}]
+
+ println(Json.decodeFromString(ListSerializer(ProjectSerializer), string))
+ // [OwnedProject(name=kotlinx.serialization, owner=kotlin), BasicProject(name=example)]
+}
+//sampleEnd
+```
+{kotlin-runnable="true"}
+
+In this example, the serializer decides which subtype to use based on the JSON content, so you don't need a `sealed` class for the class hierarchy.
+
+## Add custom behavior to the default serializer
+
+You can add custom behavior to the default serializer that Kotlin serialization generates.
+
+To do so, annotate a serializable class with the [Experimental](components-stability.md#stability-levels-explained) [`@KeepGeneratedSerializer`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-keep-generated-serializer/) and use the automatically created `generatedSerializer()` as the base serializer in your custom implementation:
+
+```kotlin
+// Imports declarations from the serialization library
+import kotlinx.serialization.*
+import kotlinx.serialization.builtins.*
+import kotlinx.serialization.json.*
+
+//sampleStart
+// Defines a sealed hierarchy where each subtype has a name property
+@Serializable
+sealed class Project {
+ abstract val name: String
+}
+
+@OptIn(ExperimentalSerializationApi::class)
+@KeepGeneratedSerializer
+@Serializable(with = BasicProjectSerializer::class)
+@SerialName("basic")
+data class BasicProject(override val name: String): Project()
+
+// Adds custom logic to the default serializer to rename a field during deserialization
+object BasicProjectSerializer : JsonTransformingSerializer(BasicProject.generatedSerializer()) {
+ override fun transformDeserialize(element: JsonElement): JsonElement {
+ val jsonObject = element.jsonObject
+ // Renames "basic-name" to "name" if it exists in the JSON input
+ return if ("basic-name" in jsonObject) {
+ val nameElement = jsonObject["basic-name"] ?: throw IllegalStateException()
+ JsonObject(mapOf("name" to nameElement))
+ } else {
+ jsonObject
+ }
+ }
+}
+
+
+fun main() {
+
+ // Deserializes JSON where the field name differs from the expected structure
+ val project = Json.decodeFromString("""{"type":"basic","basic-name":"example"}""")
+
+ println(project)
+ // BasicProject(name=example)
+}
+//sampleEnd
+```
+{kotlin-runnable="true"}
+
+In this example, the custom serializer updates the JSON structure during deserialization by renaming a field so it matches the `name` property defined in the base class.
+
+
+
+## Implement custom serialization logic in JSON
+
+
+If the transformation functions provided by `JsonTransformingSerializer` or
+`JsonContentPolymorphicSerializer` aren't enough, you can implement custom
+serialization logic by defining your own [`KSerializer`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-k-serializer/) class.
+
+This gives you full control over how values are serialized and deserialized by overriding the
+`serialize()` and `deserialize()` functions directly.
+
+When you implement custom serialization logic for JSON, you can cast `Encoder` to
+`JsonEncoder` and `Decoder` to `JsonDecoder` to call JSON-specific functions such as
+[`decodeJsonElement()`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-decoder/decode-json-element.html) and [`encodeToJsonElement()`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/encode-to-json-element.html).
+These functions allow you to retrieve and insert JSON elements at specific points in the serialization process.
+
+Both `JsonDecoder` and `JsonEncoder` expose a `json` property that gives access to the active `Json` instance, which controls how values are encoded and decoded.
+Through that instance, you can use [`encodeToJsonElement()`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/encode-to-json-element.html) and [`decodeFromJsonElement()`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/decode-from-json-element.html) to convert
+between `JsonElement` instances and Kotlin objects.
+
+Using these APIs, you can implement two-stage conversions. For example, you can:
+
+* decode the input into a `JsonElement` first and then convert that element into a Kotlin value.
+* convert a Kotlin value into a `JsonElement` first and then encode that element with the encoder.
+
+Here's an example that shows how to implement a custom `KSerializer` to fully control how a type is encoded and decoded in JSON:
+
+```kotlin
+// Imports declarations from the serialization library
+import kotlinx.serialization.*
+import kotlinx.serialization.builtins.*
+import kotlinx.serialization.json.*
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.encoding.*
+
+// Defines a sealed class for API responses
+@Serializable(with = ResponseSerializer::class)
+sealed class Response {
+ data class Ok(val data: T) : Response()
+ data class Error(val message: String) : Response()
+}
+
+// Implements custom serialization logic for Response
+@OptIn(InternalSerializationApi::class)
+class ResponseSerializer(
+ private val dataSerializer: KSerializer
+) : KSerializer> {
+
+ override val descriptor: SerialDescriptor =
+ buildSerialDescriptor("Response", PolymorphicKind.SEALED) {
+ element("Ok", dataSerializer.descriptor)
+ element("Error", buildClassSerialDescriptor("Error") {
+ element("message")
+ })
+ }
+
+ // Deserializes a Response value from JSON
+ override fun deserialize(decoder: Decoder): Response {
+ // Ensures that the decoder is a JsonDecoder
+ require(decoder is JsonDecoder)
+
+ // Decodes the input into a JsonElement
+ val element = decoder.decodeJsonElement()
+
+ // Converts the JsonElement into the corresponding Response value
+ return if (element is JsonObject && "error" in element) {
+ Response.Error(element["error"]!!.jsonPrimitive.content)
+ } else {
+ Response.Ok(
+ decoder.json.decodeFromJsonElement(dataSerializer, element)
+ )
+ }
+ }
+
+ // Serializes a Response value to JSON
+ override fun serialize(encoder: Encoder, value: Response) {
+ // Ensures that the encoder is a JsonEncoder
+ require(encoder is JsonEncoder)
+
+ // Converts the Response value into a JsonElement
+ val element = when (value) {
+ is Response.Ok ->
+ encoder.json.encodeToJsonElement(dataSerializer, value.data)
+ is Response.Error ->
+ buildJsonObject { put("error", value.message) }
+ }
+
+ // Encodes the JsonElement using the encoder
+ encoder.encodeJsonElement(element)
+ }
+}
+
+@Serializable
+data class Project(val name: String)
+
+fun main() {
+ val responses = listOf(
+ Response.Ok(Project("kotlinx.serialization")),
+ Response.Error("Not found")
+ )
+
+ val json = Json.encodeToString(responses)
+ println(json)
+ // [{"name":"kotlinx.serialization"},{"error":"Not found"}]
+
+ println(Json.decodeFromString>>(json))
+ // [Ok(data=Project(name=kotlinx.serialization)), Error(message=Not found)]
+}
+```
+{kotlin-runnable="true"}
+
+In this example, the `Response` class has a custom serializer that handles `Ok` values directly as JSON values, but handles `Error` values as JSON objects containing the error message.
+
+### Preserve unknown JSON attributes
+
+A common use case for a custom JSON-specific serializer is preserving JSON properties from the input that your serializable class doesn't define.
+By default, these properties are ignored during deserialization.
+
+To preserve these JSON properties, implement a custom JSON-specific serializer that collects all properties not defined in the target class into a dedicated [`JsonObject`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-object/) field during deserialization.
+This allows you to preserve these properties in the serializable class without modifying the original JSON structure.
+
+Here's an example:
+
+```kotlin
+// Imports declarations from the serialization library
+import kotlinx.serialization.*
+import kotlinx.serialization.builtins.*
+import kotlinx.serialization.json.*
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.encoding.*
+
+//sampleStart
+data class UnknownProject(val name: String, val details: JsonObject)
+
+object UnknownProjectSerializer : KSerializer {
+ override val descriptor: SerialDescriptor = buildClassSerialDescriptor("UnknownProject") {
+ element("name")
+ element("details")
+ }
+
+ override fun deserialize(decoder: Decoder): UnknownProject {
+ // Ensures the decoder is JSON-specific
+ val jsonInput = decoder as? JsonDecoder ?: error("Can be deserialized only by JSON")
+
+ // Reads the entire content as JSON
+ val json = jsonInput.decodeJsonElement().jsonObject
+
+ // Extracts and removes the name property
+ val name = json.getValue("name").jsonPrimitive.content
+
+ // Collects all remaining JSON properties into the details field
+ val details = json.toMutableMap()
+ details.remove("name")
+ return UnknownProject(name, JsonObject(details))
+ }
+
+ override fun serialize(encoder: Encoder, value: UnknownProject) {
+ error("Serialization is not supported")
+ }
+}
+
+fun main() {
+ // Deserializes JSON with properties not defined in the serializable class into UnknownProject
+ println(Json.decodeFromString(UnknownProjectSerializer, """{"type":"unknown","name":"example","maintainer":"Unknown","license":"Apache 2.0"}"""))
+ // UnknownProject(name=example, details={"type":"Unknown","maintainer":"Unknown","license":"Apache 2.0"})
+
+}
+//sampleEnd
+```
+{kotlin-runnable="true"}
+
+In this example, the preserved JSON properties remain at the same level within the input JSON object as the properties defined in the serializable class.
+
+
+## What's next
+
+* Learn how to [serialize polymorphic classes](serialization-polymorphism.md) and handle objects of various types within a shared hierarchy.
+* Discover [other serialization formats](alternative-serialization-formats.md), such as CBOR and ProtoBuf.