ข้าม​ไป​ยัง​เนื้อหา

สิบ​สี่​บรรทัด​ที่​เปิด​ประตู JSON — และ​บรรทัด​ที่​ลืม​แล้ว build ยัง​เขียว

สอง​บท​ที่​แล้ว orderctl ไม่มี build tool เลย เรา​พิมพ์ kotlinc เอง​แล้ว​ต่อ -cp เอง

บท​นี้​เส้น​นั้น​ขาด เพ​ราะ​ออ​เด​อร์​ใบ​จริง​อยู่​ใน JSON ไม่​ได้​อยู่​ใน source และ​ตัว​อ่าน JSON ของ Kotlin ต้องการ​ของ​สอง​ชั้น​ที่​ต้อง​มา​พร้อม​กัน

📦 บท​นี้​สร้าง​อะไร
  • build.gradle.kts สิบ​สาม​บรรทัด กับ settings.gradle.kts อีก​หนึ่ง
  • Order ที่​อ่าน​และ​เขียน JSON ได้​ทั้ง​สอง​ทาง
  • order.json ที่​โปรแกรม​อ่าน​ตอน​รัน

ทุก​ผลลัพธ์​ใน​บท​นี้​มา​จาก gradle run ไม่ใช่​จาก kotlin -cp out ของ​บท​ที่ 1–2 อีก​แล้ว · ธง -q ตัด​รายงาน​ของ Gradle ตอน​เขียว​ออก​หมด แต่​ตอน​แดง​มัน​ยัง​พิมพ์​ก้อน FAILURE: ต่อ​ท้าย ซึ่ง transcript ของ​บท​นี้​ตัด​ออก​เพื่อ​ให้​เหลือ​แต่​สิ่ง​ที่​โปรแกรม​พิมพ์​เอง

kotlinc เปล่า​ไป​ได้​ถึง​บท​ที่​แล้ว และ​หยุด​ที่​บท​นี้

หัวข้อ​ที่​มีชื่อ​ว่า “kotlinc เปล่า​ไป​ได้​ถึง​บท​ที่​แล้ว และ​หยุด​ที่​บท​นี้”

JSON ของ Kotlin ต้องการ​ของ​สอง​ชั้น และ​มี​ชั้น​เดียว​ที่​อยู่​ใน​กล่อง​ของ kotlinc

kotlinx.serializationkotlinx.serializationlibrary JSON ของ JetBrains ที่​ทำงาน​คู่​กับ compiler plugin · มัน​โยน​ทั้ง​ตอน​เจอ​คีย์​แปลก​ปลอม​และ​ตอน​คีย์​ที่​ต้อง​มี​หาย​ไป ซึ่ง​กลับ​ด้าน​กับ​ทั้ง `encoding/json` ของ Go และ Jackson ที่​ข้าม​ให้​เงียบ​ทั้ง​คู่ ไม่​ได้​เป็น​ก้อน​เดียว มัน​แยก​เป็น​สอง​ชั้น​ที่​ต้อง​มา​พร้อม​กัน

  • ชั้น​ตอน​แปล คือ compiler plugincompiler pluginส่วน​ขยาย​ของ compiler ที่​สร้าง code เพิ่ม​ตอน​แปล เช่น​ตัว​แปลง JSON ของ kotlinx.serialization · ลืม​เปิด​แล้ว​ยัง​แปล​ผ่าน รหัส​ออก 0 ไม่มี​คำ​เตือน แล้ว​โปรแกรม​ตาย​ตอน​รัน ที่​เขียน​ตัว​แปลง JSON ให้ data class ของ​เรา
  • ชั้น​ตอน​รัน คือ library สอง​ใบ​ที่​ต้อง​อยู่​บน class path

ชั้น​แรก​อยู่​ใน​กล่อง​ของ kotlinc แล้ว ชั้น​ที่​สอง​ไม่​อยู่ · การ​ดึงส​อง​ใบ​นั้น​เอง​ด้วย​มือ​ทำได้ แต่​พอ​มี​ใบ​ที่​สาม​ใบ​ที่​สี่​ตาม​มา เรา​ก็​กำลัง​เขียน build tool ด้วย​มือ​ที​ละ​บรรทัด

คอร์ส Go ไม่​เจอ​ทาง​แยก​นี้​เลย เพราะ encoding/json อยู่​ใน standard library · คอร์ส Java เจอ​ที่​บท​เดียวกัน​นี้ และ​ตอบ​ด้วย pom.xml ยี่สิบ​บรรทัด

เลือก Gradle แล้ว และ​ข้อ​โต้​แย้ง​ที่​คนใช้​ปฏิเสธ​มัน​ยัง​จริง​อยู่

ข้อ​โต้​แย้ง​ที่​คนใช้​ปฏิเสธ Gradle บ่อย​ที่สุด​คือ​มัน​มี DSL สอง​แบบ​ให้​เลือก แล้ว​ผู้​อ่าน​ต้อง​ตัดสิน​ใจ เรื่อง​ที่​ไม่​ควร​ต้อง​ตัดสิน​ใจ​ตั้งแต่​วัน​แรก · คอร์ส Java จึง​อยู่​กับ Maven ตลอด​แปดบท

ข้อ​นั้น​ยัง​จริง แต่​มัน​ไม่​กัด​ผู้​อ่าน​ของ​คอร์ส​นี้ เพราะ DSL ที่​เรา​เลือก​คือ Kotlin เอง ซึ่ง​เป็น​ภาษา​ที่​กำลัง​เรียน​อยู่ · build.gradle.kts จึง​อ่าน​ออก​ด้วย​ความ​รู้​จาก​สอง​บท​ที่​แล้ว

ราคา​ที่​จ่าย​จริง​คือ Gradle ต้อง​ลง​เพิ่ม​บน​เครื่อง ส่วน mvn มา​กับ​ที่​หลาย​คน​มี​อยู่​แล้ว · บท​นี้​ใช้ Gradle 9.7.1 ซึ่ง​ฝัง Kotlin 2.4.0 ไว้ · ตาราง​ความ​เข้า​กัน​ได้​ของ Gradle เขียน​ว่า สาย​ที่​รัน​บน Java 25 ได้​เริ่ม​ที่ 9.1.0 และ​ภาษา​ของ Kotlin DSL ที่​บังคับ​ใช้​จริง​คือ​รุ่น 2.2

สอง file รวม​กัน​สิบ​สี่​บรรทัด สั้น​กว่า pom ของ Java ที่​ยี่สิบ ทั้ง​ที่​ทำ​มากกว่า

settings.gradle.kts มี​บรรทัด​เดียว และ​มัน​คือ​สิ่ง​ที่​บอก Gradle ว่า​ตรง​นี้​คือ​ราก​ของ​โปร​เจ​กต์

settings.gradle.kts
rootProject.name = "orderctl"

build.gradle.kts มี​สิบ​สาม​บรรทัด และ​ไม่มี​บรรทัด​ไหน​ที่​ตัด​ออก​ได้​โดย​ไม่​เสีย​อะไร

build.gradle.kts
plugins {
kotlin("jvm") version "2.4.10"
kotlin("plugin.serialization") version "2.4.10"
application
}
repositories { mavenCentral() }
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
testImplementation(kotlin("test"))
}
kotlin { jvmToolchain(25) }
application { mainClass = "orderctl.MainKt" }
tasks.test { useJUnitPlatform() }

อ่าน​ที​ละ​บรรทัด​แล้ว​จะ​เห็น​ว่า​มัน​ประกาศ​ห้า​อย่าง​ที่​ต่าง​กัน​คนละ​เรื่อง

บรรทัดมัน​ซื้อ​อะไร​มา
kotlin("jvm")compiler ของ Kotlin และ​การ​ตรึง kotlin-stdlib ให้​ตรง​รุ่น​กับ compiler
kotlin("plugin.serialization")compiler plugincompiler pluginส่วน​ขยาย​ของ compiler ที่​สร้าง code เพิ่ม​ตอน​แปล เช่น​ตัว​แปลง JSON ของ kotlinx.serialization · ลืม​เปิด​แล้ว​ยัง​แปล​ผ่าน รหัส​ออก 0 ไม่มี​คำ​เตือน แล้ว​โปรแกรม​ตาย​ตอน​รัน ที่​เขียน​ตัว​แปลง JSON ให้ · บรรทัด​ที่​ทั้ง​บท​นี้​หมุน​รอบ​มัน
applicationtask run และ installDist
implementation(...json:1.11.0)library ตอน​รัน สอง​ใบ · dependency ตัว​แรก​ของ​คอร์ส​นี้
jvmToolchain(25)jvm-targetjvm-targetรุ่น​ของ bytecode ที่ compiler คาย​ออก​มา · ค่า​ปริยาย​ของ `kotlinc` บน​บรรทัด​คำ​สั่ง​คือ 1.8 และ​มัน​ไม่​เตือน จึง​ต้อง​ตั้ง​เอง​ทุก​ครั้ง ไม่​งั้น​ไป​เจอ​เป็น​ความ​ผิด​พลาด​ตอน​เรียก library สมัย​ใหม่​แทน ที่​ตรึง​ไว้ ไม่​ปล่อย​ให้​เดา​จาก​เครื่อง

เทียบ​กับ​สอง​คอร์ส​พี่​ที่​ทำงาน​เดียวกัน ตัวเลข​ห่าง​กัน​มากกว่า​ที่​คน​คาด

ภาษาสิ่ง​ที่​ต้อง​เขียนบรรทัด
Gogo mod init example.com/orderctl1 (คำ​สั่ง ไม่ใช่ file)
Javapom.xml20
Kotlin · Gradle Kotlin DSLbuild.gradle.kts 13 + settings.gradle.kts 114
Kotlin · Mavenpom.xml42

สอง​แถว​ล่าง​คือ​ภาษา​เดียวกัน​และ​งาน​เดียวกัน ต่าง​กัน​แค่ build tool · pom ของ Kotlin ยาว​กว่า​ของ Java สอง​เท่า​เศษ​เพราะ kotlin-maven-plugin ต้อง​มี <dependencies> ของ​ตัว​เอง​เพื่อ​ใส่​ตัว​ช่วย​ของ serialization แล้ว​ต้อง​ประกาศ <compilerPlugins> แยก​อีก​ชั้น

สิบ​สี่​บรรทัด​ของ Kotlin ทำ​มากกว่า​ยี่สิบ​บรรทัด​ของ Java

pom ยี่สิบ​บรรทัด​ของ​คอร์ส Java ตั้ง maven.compiler.release ประกาศ Jackson หนึ่ง​ใบ แล้ว​จบ · มัน​ไม่มี compiler plugin ไม่มี​เทสต์ และ​ไม่มี task ที่​รัน​โปรแกรม​ให้

สิบ​สี่​บรรทัด​ของ Kotlin รวม​ทั้ง​สาม​อย่าง​นั้น​ไว้​แล้ว · นี่​เป็นการ​พลิก​ข้อ​อ้าง​ของ​คอร์ส Java ที่​ขาย XML ยี่สิบ​บรรทัด​ว่า​เป็น​ราคา​ที่​ต้อง​จ่าย

gradle init เขียน​ให้​ได้ แต่​มัน​เขียน​ของ​ที่​เรา​ไม่​ได้​ขอ

หัวข้อ​ที่​มีชื่อ​ว่า “gradle init เขียน​ให้​ได้ แต่​มัน​เขียน​ของ​ที่​เรา​ไม่​ได้​ขอ”

คำ​สั่ง​ที่​เขียน build file ให้​มี​อยู่​จริง และ​ผล​ของ​มัน​ยาว​กว่า​ที่​เรา​พิมพ์​เอง​สี่​เท่า​เศษ

Gradle มี gradle init ที่​สร้าง​โปร​เจ​กต์​ให้​ทั้ง​ชุด · สั่ง​มัน​แบบ​ไม่​ระบุ DSL แล้ว​ดู​ว่า​ได้​อะไร

$ gradle -q init --type kotlin-application --project-name orderctl --package orderctl --java-version 25
$ find . -type f | sort
./.gitattributes
./.gitignore
./app/build.gradle.kts
./app/src/main/kotlin/orderctl/App.kt
./app/src/test/kotlin/orderctl/AppTest.kt
./gradle.properties
./gradle/libs.versions.toml
./gradle/wrapper/gradle-wrapper.jar
./gradle/wrapper/gradle-wrapper.properties
./gradlew
./gradlew.bat
./settings.gradle.kts

ข้อ​แรก​ที่​เห็น​คือ มัน​เขียน .kts ให้​เอง​โดย​ไม่​ต้อง​บอก · คำถาม “DSL ไหน​ดี” ที่​คนใช้​ปฏิเสธ Gradle ไม่​ได้​ถูก​โยน​ใส่​ผู้​อ่าน​อีก​แล้ว ค่า​ปริยาย​ตอบ​ให้​แล้ว

ข้อ​ที่​สอง​คือ​ขนาด · app/build.gradle.kts ยาว 49 บรรทัด และ settings.gradle.kts ยาว 14 รวม​สอง file เป็น 63 บรรทัด เทียบ​กับ 14 ที่​เรา​พิมพ์​เอง

และ​ของ​ที่​มัน​ใส่​มา​ให้​มี​สิ่ง​ที่​เรา​ไม่​ได้​ขอ

  • implementation(libs.guava) — dependency ที่ orderctl ไม่​ได้​ใช้​สัก​บรรทัด
  • gradle/libs.versions.toml — version catalog ที่​เพิ่ม​ชั้น​การ​อ้อม​อีก​ชั้น
  • ไม่มี kotlin("plugin.serialization") เลย ซึ่ง​เป็น​บรรทัด​ที่​บท​นี้​ต้องการ​ที่สุด

คอร์ส​นี้​จึง​พิมพ์​เอง · สิบ​สี่​บรรทัด​ที่​อ่าน​ออก​ทุก​บรรทัด ดี​กว่า​หก​สิบ​สาม​บรรทัด​ที่​ต้อง​ไล่​ลบ

ลบ​บรรทัด​นี้​แล้ว build ยัง​เขียว แต่​ผล​ที่​ได้​ผูก​กับ JDK ที่​รัน Gradle อยู่ ไม่ใช่ JDK ที่​เรา​ตั้งใจ

ค่า​ปริยาย​ของ jvm-targetjvm-targetรุ่น​ของ bytecode ที่ compiler คาย​ออก​มา · ค่า​ปริยาย​ของ `kotlinc` บน​บรรทัด​คำ​สั่ง​คือ 1.8 และ​มัน​ไม่​เตือน จึง​ต้อง​ตั้ง​เอง​ทุก​ครั้ง ไม่​งั้น​ไป​เจอ​เป็น​ความ​ผิด​พลาด​ตอน​เรียก library สมัย​ใหม่​แทน บน kotlinc เปล่า​คือ 1.8 และ​มัน​เงียบ​สนิท ซึ่ง​เป็น​เหตุผล​ที่​บท​ที่ 2 เขียน -jvm-target 25 ไว้​ทุก​คำ​สั่ง · ฝั่ง Gradle ความ​เงียบ​ยัง​อยู่ แต่​ค่าที่​มัน​ตก​ไป​หา​เปลี่ยน​ไป

ลบ kotlin { jvmToolchain(25) } ออก​จาก build.gradle.kts แล้ว​สั่ง build บน​เครื่อง​ที่ Gradle รัน​บน JDK 26 อยู่ · ผล​คือ build เขียว รหัส​ออก 0 ไม่​พิมพ์​อะไร​เลย แล้ว class ที่​ได้​รัน​บน JDK 25 ไม่​ได้

$ java -cp build/classes/kotlin/main:... orderctl.MainKt
Error: LinkageError occurred while loading main class orderctl.MainKt
java.lang.UnsupportedClassVersionError: orderctl/MainKt has been compiled by a more recent version of the Java Runtime (class file version 70.0), this version of the Java Runtime only recognizes class file versions up to 69.0

ข้อความ​นั้น​ไม่​เอ่ย​ถึง Gradle ไม่​เอ่ย​ถึง Kotlin และ​ไม่​เอ่ย​ถึง​บรรทัด​ที่​หาย​ไป · มัน​บอก​แค่​เลข​สอง​ตัว​ที่​ผู้​อ่าน​ต้อง​แปล​เอง

jvmToolchain(25) ตัด​ความ​ไม่​แน่นอน​นี้​ทิ้ง · มี​บรรทัด​นี้​แล้ว​ผล​เป็น 69 เสมอ ไม่​ว่า Gradle บน​เครื่อง​นั้น​รัน​อยู่​บน JDK ตัว​ไหน

สภาพ​ของ buildรหัส​ออก​ของ buildmajor version ที่​ได้
kotlinc เปล่า ไม่​ระบุ -jvm-target052
Gradle ไม่มี jvmToolchain(25) · daemon บน JDK 26070
Gradle มี jvmToolchain(25)069

สาม​แถว​นี้​ให้​รหัส​ออก 0 เหมือน​กัน​หมด · หน้า​จอ​แยก​ไม่​ออก ต้อง​เปิด class file ดู​ถึง​จะ​รู้

Kotlin ไม่​บังคับ​หนึ่ง​ชนิด​ต่อ​หนึ่ง file · Order กับ main จึง​อยู่​ใน file เดียวกัน​ได้

คอร์ส Java ต้อง​แตก​โปรแกรม​จาก​หนึ่ง file เป็น​สาม เพราะ​กฎ public type หนึ่ง​ตัว​ต่อ​หนึ่ง file · Kotlin ไม่มี​กฎ​นั้น Order กับ main จึง​อยู่​ด้วย​กัน​ได้

src/main/kotlin/orderctl/Main.kt
package orderctl
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.nio.file.Path
import kotlin.io.path.readText
@Serializable
data class Order(
val id: String,
@SerialName("customer_name") val customerName: String,
val totalSatang: Long,
val note: String = "",
)
fun main() {
val order = Json.decodeFromString<Order>(Path.of("order.json").readText())
println("อ่านออเดอร์ ${order.id} ของ ${order.customerName} ยอด ${order.totalSatang}")
println(Json.encodeToString(order))
}

@SerialName("customer_name") ทำ​สิ่ง​เดียว​กับ struct tag ของ Go คือ​แยก​ชื่อ​คีย์​ใน JSON ออก​จาก​ชื่อ property ใน Kotlin · คอร์ส Java เขียน​ไว้​เอง​ว่า​ฝั่ง Java ไม่มี​ของ​แบบ​นี้ ชื่อ component ของ record ต้อง​ตรง​กับ​คีย์

วา​งอ​อเด​อร์​ใบ​แรก​ไว้​ข้าง build.gradle.kts แล้ว​สั่ง gradle run

$ cat order.json
{"id":"ORD-1042","customer_name":"ร้านชาใต้ถุน","totalSatang":7000}
$ gradle -q run
อ่านออเดอร์ ORD-1042 ของ ร้านชาใต้ถุน ยอด 7000
{"id":"ORD-1042","customer_name":"ร้านชาใต้ถุน","totalSatang":7000}

บรรทัด​ที่​สอง​คือ​ขา​เขียน · encodeToString เขียน customer_name กลับ​ออก​มา ไม่ใช่ customerName ป้าย​เดียว​จึง​คุม​ทั้ง​สอง​ทาง

build เขียว รหัส​ออก 0 ไม่มี​คำ​เตือน แล้ว​โปรแกรม​ตาย

หัวข้อ​ที่​มีชื่อ​ว่า “build เขียว รหัส​ออก 0 ไม่มี​คำ​เตือน แล้ว​โปรแกรม​ตาย”

ลบ​บรรทัด​ของ compiler plugin ออก​บรรทัด​เดียว แล้ว​ไม่มี​เครื่อง​มือ​ไหน​บอก​อะไร​เลย​จนกว่า​จะ​รัน

นี่​คือ​ความ​ผิด​พลาด​ชนิด​ที่​สอง​คอร์ส​พี่​ไม่มี · Go กับ Java อ่าน JSON ด้วย reflection ตอน​รัน ส่วน Kotlin ให้ compiler plugincompiler pluginส่วน​ขยาย​ของ compiler ที่​สร้าง code เพิ่ม​ตอน​แปล เช่น​ตัว​แปลง JSON ของ kotlinx.serialization · ลืม​เปิด​แล้ว​ยัง​แปล​ผ่าน รหัส​ออก 0 ไม่มี​คำ​เตือน แล้ว​โปรแกรม​ตาย​ตอน​รัน เขียน​ตัว​แปลง​ลง​ไป​ตั้งแต่​ตอน​แปล

flowchart TD
  S["Main.kt<br/>@Serializable data class Order"] --> P{"build.gradle.kts<br/>มีบรรทัด plugin.serialization ไหม"}
  P -->|"มี"| A["compiler plugin เขียน serializer<br/>ลงไปใน Order"]
  P -->|"ไม่มี"| B["ไม่มีใครเขียน serializer<br/>และไม่มีใครบ่น"]
  A --> C["gradle -q build<br/>รหัสออก 0 · ไม่พิมพ์อะไรเลย"]
  B --> D["gradle -q build<br/>รหัสออก 0 · ไม่พิมพ์อะไรเลย"]
  C --> E["gradle run พิมพ์ออเดอร์<br/>รหัสออก 0"]
  D --> F["gradle run โยน SerializationException<br/>รหัสออก 1"]

คำ​บรรยาย​ภาพ: สอง​เส้น​ทาง​แยก​กัน​ตั้งแต่ build file · แต่​ผล​ของ build เหมือน​กัน​ทุก​ไบต์ ต่าง​กัน​ตอน​รัน

ลบ​บรรทัด kotlin("plugin.serialization") version "2.4.10" ออก เหลือ build.gradle.kts สิบ​สอง​บรรทัด แล้ว build

$ gradle -q build
$ echo $?
0

ไม่มี​อะไร​พิมพ์​ออก​มา​เลย · ไม่ใช่​คำ​เตือน​หนึ่ง​บรรทัด ไม่ใช่​ข้อความ info ที่​อ่าน​ข้าม​ได้ แต่​เป็น​ความ​ว่าง​เปล่า​ที่​หน้าตา​เหมือน build ที่​ถูก​ต้อง​ทุก​ประการ

แล้ว​รัน

$ gradle -q run
Exception in thread "main" kotlinx.serialization.SerializationException: Serializer for class 'Order' is not found.
Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.
at kotlinx.serialization.internal.Platform_commonKt.serializerNotRegistered(Platform.common.kt:90)
at kotlinx.serialization.internal.PlatformKt.platformSpecificSerializerNotRegistered(Platform.kt:33)
at kotlinx.serialization.SerializersKt__SerializersKt.serializer(Serializers.kt:153)
at kotlinx.serialization.SerializersKt.serializer(Unknown Source)
at orderctl.MainKt.main(Main.kt:23)
at orderctl.MainKt.main(Main.kt)

ข้อความ​นี้​ดี​กว่า​ที่​ควร​จะ​เป็น มัน​บอก​ทั้ง​สอง​เงื่อนไข​ที่​ต้อง​มี · แต่​มัน​มา​ถึง​ตอน​รัน หลัง​จาก build เขียว​ไป​แล้ว และ​หลัง​จาก​ของ​ถูก​ส่ง​ไป​ที่​อื่น​แล้ว

ทำไม `-Werror` ก็​จับ​ไม่​ได้

บท​ที่ 2 แปล​ทุก​ครั้ง​ด้วย kotlinc -Werror -Wextra เพื่อ​ไม่​ให้​คำ​เตือน​สัก​ตัว​หลุด​ไป​ได้ · ธง​คู่​นั้น​ก็​ยัง​เงียบ​กับ​เรื่อง​นี้

@Serializable เป็น annotation ธรรมดา​ตัว​หนึ่ง · เมื่อ​ไม่มี plugin มา​อ่าน​มัน compiler ก็​เห็น​มัน​เป็น annotation ที่​ไม่มี​ใคร​ใช้ ซึ่ง​ไม่ใช่​ความ​ผิด​พลาด และ​ไม่ใช่​คำ​เตือน

ส่วน Json.decodeFromString<Order> ก็​เป็นการ​เรียก​ฟังก์ชัน​ที่​ถูก​ต้อง​ทุก​อย่าง · ตัว​ที่​หาย​ไป​คือ serializer ซึ่ง​ถูก​ค้นหา​ตอน​รัน ไม่ใช่​ตอน​แปล

กฎ​ที่​ใช้ได้​จริง​คือ @Serializable กับ​บรรทัด plugin.serialization ต้อง​เดิน​ทาง​คู่​กัน​เสมอ · เพิ่ม annotation ใน​โปร​เจ​กต์​ใหม่​เมื่อไร ให้​เปิด build file ดู​ทันที

Kotlin เป็น​ภาษา​เดียว​ใน​สาม​ภาษา​ที่​ค่า​ปริยาย​โยน​ทั้ง​สอง​ทาง สิ่ง​ที่​ต้อง​เรียน​จึง​เป็น​วิธี​ปิด ไม่ใช่​วิธี​เปิด

เติม​คีย์​ที่ Order ไม่รู้​จัก​ลง​ใน order.json แล้ว​รัน

$ cat order.json
{"id":"ORD-1042","customer_name":"ร้านชาใต้ถุน","totalSatang":7000,"discount":500}
$ gradle -q run
Exception in thread "main" kotlinx.serialization.json.JsonDecodingException: Unexpected JSON token at offset 68: Encountered an unknown key 'discount' at path: $
Use 'ignoreUnknownKeys = true' in 'Json {}' builder or '@JsonIgnoreUnknownKeys' annotation to ignore unknown keys.
JSON input: {"id":"ORD-1042","customer_name":"ร้านชาใต้ถุน","totalSatang":7000,"discount":500}

ทีนี้​ลบ​คีย์​ที่​บังคับ​ออก​แทน

$ cat order.json
{"id":"ORD-1042","customer_name":"ร้านชาใต้ถุน"}
$ gradle -q run
Exception in thread "main" kotlinx.serialization.MissingFieldException: Field 'totalSatang' is required for type with serial name 'orderctl.Order', but it was missing at path: $

สอง​ข้อความ​นี้​คือ​จุด​ที่​คอร์ส​นี้​ต่าง​จาก​สอง​คอร์ส​พี่​ชัด​ที่สุด

ภาษา · libraryคีย์​แปลก​ปลอม ตาม​ค่า​ปริยายคีย์​ที่​หาย​ไป ตาม​ค่า​ปริยาย
Go · encoding/json Unmarshalข้าม​เงียบ ปิด​ไม่​ได้ ต้อง​ย้าย​ไป NewDecoderได้ zero value แยก​จาก​ค่าที่​ส่ง​มา​จริง​ไม่​ออก
Java · Jackson 3ข้าม​เงียบได้​ค่า​เริ่ม​ต้น​ของ​ชนิด · null ระเบิด​ทีหลัง ส่วน int ตอบ 0 เงียบ
Kotlin · kotlinx.serializationโยน​ทันทีโยน​ทันที

คอร์ส Java มี​ทั้ง​หัวข้อ​ชื่อ “เปิด​ความ​เข้ม​งวด​กลับ​มา” เพราะ Jackson 3 พลิก​ค่า​ปริยาย​ของ FAIL_ON_UNKNOWN_PROPERTIES เป็น false · คอร์ส​นี้​ไม่​ต้อง​มี​หัวข้อ​นั้น

สิ่ง​ที่​ต้อง​สอน​แทน​คือ​วิธี​ผ่อน · ระบบ​ต้นทาง​ที่​เติม​คีย์​ใหม่​โดย​ไม่​บอก​ล่วง​หน้า​มี​จริง และ​เรา​ไม่​อยาก​ให้ orderctl ตาย​เพราะ​คีย์​ที่​ไม่​เกี่ยว​กับ​เรา

src/main/kotlin/orderctl/Relaxed.kt
package orderctl
import kotlinx.serialization.json.Json
val relaxed = Json { ignoreUnknownKeys = true }

เปลี่ยน Json.decodeFromString ใน main เป็น relaxed.decodeFromString แล้ว​คีย์ discount จะ​ถูก​ข้าม​ไป · แต่​คีย์​ที่​บังคับ​ยัง​โยน​เหมือน​เดิม เพราะ​สอง​สวิตช์​นี้​แยก​จาก​กัน

ราคา​ที่ kotlinx เก็บ และ​คอร์ส Java ไม่​เก็บ

บท​ที่ 2 ให้ init { require(...) } ปฏิเสธ​ค่าที่​ผิด​ตั้งแต่​ตอน​สร้าง · ตัว​ตรวจ​นั้น​ทำงาน ตอน​อ่าน JSON ด้วย เพราะ kotlinx เรียก constructor ตัว​จริง ไม่​ได้​ยัด​ค่า​ลง property ตรงๆ

แต่​สิ่ง​ที่​ออก​มา​คือ java.lang.IllegalArgumentException ดิบ​ที่​มี​แต่​ข้อความ​ของ​เรา​เอง ไม่มี​ชื่อ​ชนิด​กำกับ และ​ไม่มี​คำ​บอก​ว่า​คีย์​ไหน​ของ JSON เป็นต้น​เหตุ

ฝั่ง Jackson รายงาน​เรื่อง​เดียวกัน​ว่า Cannot construct instance of orderctl.Line, problem: แล้ว​ต่อ​ด้วย​ข้อความ​ของ​เรา · ชื่อ​ชนิด​อยู่​ใน​บรรทัด​แรก​ของ​คอร์ส Java ส่วน​คอร์ส​นี้​ต้อง​ไล่​ไป​อ่าน ที่ stack trace เอง ซึ่ง​เฟรม​บน​สุด​เขียน​ว่า orderctl.Order.<init>

ตัว​ที่​โผล่​แทน​ใน stack trace คือ Order$$serializer ซึ่ง​เป็น class ที่ compiler plugincompiler pluginส่วน​ขยาย​ของ compiler ที่​สร้าง code เพิ่ม​ตอน​แปล เช่น​ตัว​แปลง JSON ของ kotlinx.serialization · ลืม​เปิด​แล้ว​ยัง​แปล​ผ่าน รหัส​ออก 0 ไม่มี​คำ​เตือน แล้ว​โปรแกรม​ตาย​ตอน​รัน เขียน​ขึ้น · ไม่มี​บรรทัด​ไหน​ใน source พูด​ถึง​ชื่อ​นั้น​เลย

note มี​ค่า​ปริยาย​อยู่​ใน​ลาย​เซ็น มัน​จึง​หาย​ไป​จาก JSON ได้​ทั้ง​ขา​อ่าน​และ​ขา​เขียน

Order ของ​บท​นี้​ประกาศ val note: String = "" ไว้ · ค่า​ปริยาย​ตัว​นั้น​เปลี่ยน​สถานะ​ของ​คีย์ จาก​บังคับ​เป็น​ไม่​บังคับ โดย​ไม่​ต้อง​เขียน​อะไร​เพิ่ม​เลย

$ cat order.json
{"id":"ORD-1042","customer_name":"ร้านชาใต้ถุน","totalSatang":7000}
$ gradle -q run
อ่านออเดอร์ ORD-1042 ของ ร้านชาใต้ถุน ยอด 7000
{"id":"ORD-1042","customer_name":"ร้านชาใต้ถุน","totalSatang":7000}

ขา​เขียน​ก็​ตัด​มัน​ออก​ด้วย เพราะ​ค่าที่​ถือ​อยู่​เท่ากับ​ค่า​ปริยาย​พอดี · ใส่​ค่า​กลับ​เข้าไป​แล้ว​มัน​โผล่​มา

$ cat order.json
{"id":"ORD-1042","customer_name":"ร้านชาใต้ถุน","totalSatang":7000,"note":"เผ็ดน้อย"}
$ gradle -q run
อ่านออเดอร์ ORD-1042 ของ ร้านชาใต้ถุน ยอด 7000
{"id":"ORD-1042","customer_name":"ร้านชาใต้ถุน","totalSatang":7000,"note":"เผ็ดน้อย"}

เทียบ​กับ omitempty ของ Go แล้ว​ต่าง​กัน​ที่​ว่า​ใคร​เป็น​คน​บอก · ฝั่ง Go เขียน​ป้าย​กำกับ​ที่ field ฝั่ง Kotlin ใช้​ค่า​ปริยาย​ของ constructor ที่​ประกาศ​ไว้​อยู่​แล้ว

และ​นี่​คือ​ปุ่ม​ที่​กด​ผิด​ง่าย​ที่สุด​ใน​บท​นี้ · ใส่​ค่า​ปริยาย​ให้ field ที่​ต้อง​มี​จริง แล้ว​ความ​เข้ม​งวด​ของ MissingFieldException หาย​ไป​จาก​คีย์​นั้น​ทันที โดย​ไม่มี​ใคร​เตือน

application plugin ที่​ใส่​ไว้​ตั้งแต่​แรก ให้ script ที่​เรียก​ได้​ทันที​โดย​ไม่​ต้อง​เติม​อะไร​ใน build file

คอร์ส Java เจอ​สอง​ด่าน​ตรง​นี้ · java -jar target/orderctl-1.0.0.jar ตอบ no main manifest attribute, in target/orderctl-1.0.0.jar ก่อน แล้ว​พอ​เลี่ยง​ไป​ชี้ class path ตรงๆ ก็​ยัง​ได้ NoClassDefFoundError ตาม​มา​อีก​ด่าน

คอร์ส​นั้น​ปิด​สอง​ด่าน​ได้​จริง​ใน​บท​เดียวกัน แต่​จ่าย​ด้วย​ก้อน <build> ที่​ทำให้ pom โต​จาก ยี่สิบ​บรรทัด​เป็น​สามสิบ​เจ็ด บวก​อีก​หนึ่ง​คำ​สั่ง​เพื่อ copy jar ลง lib/ และ​ของ​ที่​ได้​ยัง​เป็น​สอง​ก้อน

ฝั่ง​นี้ application plugin ที่​เรา​ใส่​ไว้​แต่​แรก​ให้ installDist มา​ด้วย โดย​ไม่​ต้อง​เติม​บรรทัด​ไหน​อีก

$ cat order.json
{"id":"ORD-1042","customer_name":"ร้านชาใต้ถุน","totalSatang":7000}
$ gradle -q installDist
$ ./build/install/orderctl/bin/orderctl
อ่านออเดอร์ ORD-1042 ของ ร้านชาใต้ถุน ยอด 7000
{"id":"ORD-1042","customer_name":"ร้านชาใต้ถุน","totalSatang":7000}

ของ​ที่​ได้​ไม่ใช่ file เดียว มัน​เป็น​ทั้ง directory ที่​มี script สอง​ใบ​กับ jar ห้า​ใบ · เลข​ของ orderctl.jar ข้าง​ล่าง​นับ​ตอน​ที่ Relaxed.kt ของ​หัวข้อ​ที่​แล้ว​อยู่​ใน​โปร​เจ​กต์​แล้ว · ถ้า​คุณ​ยัง​ไม่​ได้​สร้าง file นั้น จะ​ได้ 8,036 ไบต์​แทน ส่วน​อีก​สี่​ใบ​ไม่​ขยับ

ใบไบต์
kotlin-stdlib-2.4.10.jar1,841,933
kotlinx-serialization-core-jvm-1.11.0.jar404,509
kotlinx-serialization-json-jvm-1.11.0.jar294,166
annotations-13.0.jar17,536
orderctl.jar9,047

orderctl.jar เป็น​ใบ​ที่​เล็ก​ที่สุด​ใน​กอง · ของ​ที่​เรา​เขียน​เอง​มี​น้อย​กว่า​ของ​ที่​เรา​ลาก​มา​ด้วย​มาก ซึ่ง​เป็น​ข้อ​เดียว​กับ​ที่​บท​ที่ 1 วัด​ไว้​ด้วย -include-runtime

Gradle ปิด​กับ​ดัก​ที่ Maven เปิด​ไว้

บรรทัด​แรก​ของ​ตาราง​เขียน​ว่า kotlin-stdlib-2.4.10.jar ซึ่ง​ตรง​กับ​รุ่น​ของ compiler พอดี

ฝั่ง Maven ไม่​ตรง · ประกาศ kotlinx-serialization-json:1.11.0 ใบ​เดียว แล้ว Maven ลาก kotlin-stdlib-2.3.20 มา​ให้ เพราะ​มัน​มา​เป็น dependency ของ serialization ไม่​ได้​มา​จาก compiler

kotlin("jvm") ของ Gradle ตรึง stdlib ให้​ตาม​รุ่น​ของ​ตัว​เอง · ผู้​อ่าน​จึง​ไม่​ต้อง​รู้​เรื่อง​นี้​เลย ซึ่ง​เป็น​เหตุผล​ที่​ดี​ที่สุด​ข้อ​หนึ่ง​ของ​การ​เลือก Gradle ใน​คอร์ส​นี้

บท​นี้​เพิ่ม build tool กับ JSON และ​เพิ่ม​ความ​ผิด​พลาด​ชนิด​ใหม่​หนึ่ง​ชนิด​ที่​ไม่มี​ใคร​เตือน

orderctl ตอน​นี้​มี build.gradle.kts สิบ​สาม​บรรทัด settings.gradle.kts หนึ่ง​บรรทัด และ Order ที่​อ่าน​กับ​เขียน JSON ได้​ทั้ง​สอง​ทาง

สาม​ข้อ​ที่​ต้อง​ติดตัว​ไป​บท​หน้า

  • @Serializable กับ​บรรทัด plugin.serialization เดิน​ทาง​คู่​กัน​เสมอ · ขาด​ข้าง​เดียว​แล้ว build ยัง​เขียว
  • ค่า​ปริยาย​ของ kotlinx เข้ม​งวด​ทั้ง​สอง​ทาง · สิ่ง​ที่​ต้อง​เรียน​คือ​วิธี​ผ่อน ไม่ใช่​วิธี​ขัน
  • ค่า​ปริยาย​ของ property เท่ากับ​การ​ปลด​คีย์​นั้น​ออก​จาก​รายการ​บังคับ · ใส่​เมื่อ​จงใจ​เท่านั้น

บท​หน้า​เรา​คิด​ยอด​จา​กอ​อเด​อร์ — collection กับ Sequence · จุด​ที่​คน​มา​จาก Java ต้อง​ปรับ​ความ​คิด​คือ​ค่า​ปริยาย​ของ Kotlin เป็น​แบบ eager ต่าง​จาก Stream ที่ lazy มา​แต่​ต้น


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้

บท​นี้​อิง​ต้นทาง​ที่​ลง​วัน​ที่​กำกับ อ่าน​ต่อ​ได้​โดยตรง:

  • Gradle — Java toolchains (เข้า​ถึง 2026-08-28) — jvmToolchain และ​การ​ตรวจ​หา JDK บน​เครื่อง
  • Gradle — compatibility matrix (เข้า​ถึง 2026-08-28) — สาย​ของ Gradle ที่​รัน​บน Java 25 ได้ และ​รุ่น​ของ Kotlin DSL ที่​ฝัง​มา
  • Kotlin serialization — get started (เข้า​ถึง 2026-08-28) — plugin กับ library สอง​ชั้น​ที่​ต้อง​มา​พร้อม​กัน
  • kotlinx.serialization — JSON features (เข้า​ถึง 2026-08-28) — ignoreUnknownKeys และ​ค่า​ปริยาย​ของ encodeDefaults
  • บท​ที่ 3 ของ​คอร์ส Java — pom ยี่สิบ​บรรทัด หัวข้อ “เปิด​ความ​เข้ม​งวด​กลับ​มา” ฉาก no main manifest attribute และ​ก้อน <build> ที่​ปิด​มัน
  • บท​ที่ 3 ของ​คอร์ส Go — struct tag และ​สวิตช์ DisallowUnknownFields ที่​มี​เฉพาะ​บน Decoder
  • ตัวเลข​บรรทัด ตัวเลข​ไบต์ major version และ​ข้อความ​ข้อ​ยกเว้น​ทุก​บรรทัด วัด​เอง​เมื่อ 2026-08-28 บน Gradle 9.7.1 · Kotlin 2.4.10 · JDK 25 · macOS arm64

เช็กความเข้าใจ — บทที่ 3

ข้อ 1 / 3

ลบบรรทัด `kotlin("plugin.serialization") version "2.4.10"` ออกจาก `build.gradle.kts` แล้วสั่ง `gradle -q build` จะเกิดอะไรขึ้น