สิบสี่บรรทัดที่เปิดประตู 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 บ่อยที่สุดคือมันมี 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 ว่าตรงนี้คือรากของโปรเจกต์
rootProject.name = "orderctl"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 ให้ · บรรทัดที่ทั้งบทนี้หมุนรอบมัน |
application | task run และ installDist |
implementation(...json:1.11.0) | library ตอนรัน สองใบ · dependency ตัวแรกของคอร์สนี้ |
jvmToolchain(25) | jvm-targetjvm-targetรุ่นของ bytecode ที่ compiler คายออกมา · ค่าปริยายของ `kotlinc` บนบรรทัดคำสั่งคือ 1.8 และมันไม่เตือน จึงต้องตั้งเองทุกครั้ง ไม่งั้นไปเจอเป็นความผิดพลาดตอนเรียก library สมัยใหม่แทน ที่ตรึงไว้ ไม่ปล่อยให้เดาจากเครื่อง |
เทียบกับสองคอร์สพี่ที่ทำงานเดียวกัน ตัวเลขห่างกันมากกว่าที่คนคาด
| ภาษา | สิ่งที่ต้องเขียน | บรรทัด |
|---|---|---|
| Go | go mod init example.com/orderctl | 1 (คำสั่ง ไม่ใช่ file) |
| Java | pom.xml | 20 |
| Kotlin · Gradle Kotlin DSL | build.gradle.kts 13 + settings.gradle.kts 1 | 14 |
| Kotlin · Maven | pom.xml | 42 |
สองแถวล่างคือภาษาเดียวกันและงานเดียวกัน ต่างกันแค่ build tool
· pom ของ Kotlin ยาวกว่าของ Java สองเท่าเศษเพราะ kotlin-maven-plugin ต้องมี <dependencies>
ของตัวเองเพื่อใส่ตัวช่วยของ serialization แล้วต้องประกาศ <compilerPlugins> แยกอีกชั้น
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")เลย ซึ่งเป็นบรรทัดที่บทนี้ต้องการที่สุด
คอร์สนี้จึงพิมพ์เอง · สิบสี่บรรทัดที่อ่านออกทุกบรรทัด ดีกว่าหกสิบสามบรรทัดที่ต้องไล่ลบ
jvmToolchain(25) — บรรทัดเดียวที่กัน bytecode ผิดรุ่น
หัวข้อที่มีชื่อว่า “jvmToolchain(25) — บรรทัดเดียวที่กัน bytecode ผิดรุ่น”ลบบรรทัดนี้แล้ว 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.MainKtError: 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 | รหัสออกของ build | major version ที่ได้ |
|---|---|---|
kotlinc เปล่า ไม่ระบุ -jvm-target | 0 | 52 |
Gradle ไม่มี jvmToolchain(25) · daemon บน JDK 26 | 0 | 70 |
Gradle มี jvmToolchain(25) | 0 | 69 |
สามแถวนี้ให้รหัสออก 0 เหมือนกันหมด · หน้าจอแยกไม่ออก ต้องเปิด class file ดูถึงจะรู้
Order กับ order.json
หัวข้อที่มีชื่อว่า “Order กับ order.json”Kotlin ไม่บังคับหนึ่งชนิดต่อหนึ่ง file ·
Orderกับmainจึงอยู่ใน file เดียวกันได้
คอร์ส Java ต้องแตกโปรแกรมจากหนึ่ง file เป็นสาม เพราะกฎ public type หนึ่งตัวต่อหนึ่ง file
· Kotlin ไม่มีกฎนั้น Order กับ main จึงอยู่ด้วยกันได้
package orderctl
import kotlinx.serialization.SerialNameimport kotlinx.serialization.Serializableimport kotlinx.serialization.json.Jsonimport java.nio.file.Pathimport kotlin.io.path.readText
@Serializabledata 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 runException 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 เขียวไปแล้ว และหลังจากของถูกส่งไปที่อื่นแล้ว
บทที่ 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 runException 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 runException 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 ตายเพราะคีย์ที่ไม่เกี่ยวกับเรา
package orderctl
import kotlinx.serialization.json.Json
val relaxed = Json { ignoreUnknownKeys = true }เปลี่ยน Json.decodeFromString ใน main เป็น relaxed.decodeFromString แล้วคีย์ discount
จะถูกข้ามไป · แต่คีย์ที่บังคับยังโยนเหมือนเดิม เพราะสองสวิตช์นี้แยกจากกัน
บทที่ 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 พูดถึงชื่อนั้นเลย
field ที่มีค่าปริยาย ไม่ใช่คีย์ที่บังคับ
หัวข้อที่มีชื่อว่า “field ที่มีค่าปริยาย ไม่ใช่คีย์ที่บังคับ”
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 หายไปจากคีย์นั้นทันที โดยไม่มีใครเตือน
จบบทที่ 3 ด้วยของที่รันได้
หัวข้อที่มีชื่อว่า “จบบทที่ 3 ด้วยของที่รันได้”
applicationplugin ที่ใส่ไว้ตั้งแต่แรก ให้ 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.jar | 1,841,933 |
kotlinx-serialization-core-jvm-1.11.0.jar | 404,509 |
kotlinx-serialization-json-jvm-1.11.0.jar | 294,166 |
annotations-13.0.jar | 17,536 |
orderctl.jar | 9,047 |
orderctl.jar เป็นใบที่เล็กที่สุดในกอง · ของที่เราเขียนเองมีน้อยกว่าของที่เราลากมาด้วยมาก
ซึ่งเป็นข้อเดียวกับที่บทที่ 1 วัดไว้ด้วย -include-runtime
บรรทัดแรกของตารางเขียนว่า 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` จะเกิดอะไรขึ้น