Serialisasi dan JSON

Round-trip aeson dan API berbasis tipe.

Daftar isi

Pertukaran data antarprogram berlangsung melalui format teks yang disepakati, dan JSON merupakan format yang paling lazim untuk keperluan itu. Pustaka aeson menghubungkan JSON dengan sistem tipe Haskell secara langsung, yaitu tipe data proyek dinyatakan dapat diubah menjadi JSON melalui ToJSON dan kembali dari JSON melalui FromJSON. Topik ini membahas instans keduanya, perjalanan bolak-balik yang disebut round-trip, serta keamanan yang diperoleh dari pemisahan data mentah dan data tervalidasi.

Instans ToJSON dan FromJSON#

Tipe data berikut menyatakan jadwal piket, dan instansnya mengatur dua arah pertukaran. Instans ToJSON menyusun nilai menjadi objek JSON, sedangkan instans FromJSON membaca objek tersebut kembali menjadi nilai, dengan nama field pada JSON dinyatakan secara eksplisit di kedua tempat.

haskell piket-json.hs
{-# LANGUAGE OverloadedStrings #-}

import Data.Aeson
import qualified Data.ByteString.Lazy.Char8 as BL

data Piket = Piket
  { namaPiket :: String
  , jamPiket  :: Int
  } deriving (Show, Eq)

instance ToJSON Piket where
  toJSON (Piket nama jam) =
    object ["nama" .= nama, "jam" .= jam]

instance FromJSON Piket where
  parseJSON = withObject "Piket" $ \o -> Piket
    <$> o .: "nama"
    <*> o .: "jam"

main :: IO ()
main = do
  let asli = Piket "Ali" 21
      tersandi = encode asli
  BL.putStrLn tersandi
  print (eitherDecode tersandi :: Either String Piket)
text Keluaran
{"jam":21,"nama":"Ali"}
Right (Piket {namaPiket = "Ali", jamPiket = 21})

Fungsi encode menghasilkan ByteString berisi teks JSON, dan fungsi decode atau eitherDecode membacanya kembali. Kembalian eitherDecode berupa Either String Piket, yaitu Right berisi nilai yang berhasil dibaca atau Left berisi pesan kegagalan, bentuk yang sama dengan pola validasi pada topik data tervalidasi. Urutan field pada keluaran mengikuti urutan internal pustaka dan tidak dijanjikan, sehingga perbandingan keluaran program tidak boleh bergantung pada susunannya.

Nama field pada kedua instans wajib ditulis sama persis, karena aeson tidak menghubungkan kedua arah secara otomatis. Penulisan eksplisit seperti di atas memang lebih panjang, tetapi bentuk JSON-nya sepenuhnya di bawah kendali proyek, misalnya field jamPiket pada tipe Haskell dikirim sebagai "jam" pada JSON tanpa mengubah nama fieldnya. Konvensi penamaan yang berbeda antara kedua dunia, misalnya gaya camelCase dan snake_case, ditampung pada tempat yang sama, yaitu instans kedua arahnya. Perubahan nama di salah satu arah saja tidak ditolak compiler, keadaan yang hanya terlihat pada saat program dijalankan.

Round-trip dan jaminannya#

Perjalanan bolak-balik dari nilai ke JSON dan kembali lagi merupakan pengujian kebenaran yang paling dasar, yaitu decode (encode x) seharusnya menghasilkan x semula. Properti tersebut menyatakan sifat yang dapat diuji dengan QuickCheck, karena untuk segenap nilai Piket yang dapat dibangun, perjalanan bolak-baliknya tidak boleh mengubah apa pun. Keluaran program di atas memperlihatkan satu kasusnya, yaitu Right berisi nilai yang persis sama dengan yang dikirim.

Jaminan itu bergantung pada kesetaraan kedua arah, dan kesetaraan itu dapat terganggu pada dua tempat. Pertama, field yang tidak dibaca oleh FromJSON akan hilang pada perjalanan balik, keadaan yang tidak terlihat dari satu contoh yang lolos. Kedua, tipe yang informasinya lebih luas pada JSON daripada pada tipe Haskell, misalnya angka desimal yang diubah menjadi Int, akan mengalami pemangkasan yang tidak terdeteksi. Pola round-trip beserta propertinya menutup kedua celah itu karena setiap field diuji kembali.

Properti bolak-balik ditulis sebagai fungsi Bool biasa, misalnya prop_roundTrip :: Piket -> Bool dengan isi eitherDecode (encode x) == Right x, lalu diserahkan kepada QuickCheck sebagaimana properti lainnya. Instans Arbitrary untuk Piket membangun nilai acak dari kombinasi nama dan jam yang sah, sehingga seluruh ruang nilai diuji tanpa satu pun contoh yang ditulis tangan. Bentuk ini menjadi templat umum bagi tipe data lain yang dihubungkan dengan JSON.

Dari JSON mentah ke data tervalidasi#

Pembacaan JSON dari luar, misalnya respons sebuah layanan, menghasilkan data yang belum dijamin isinya. Alur yang aman berupa membaca teks JSON menjadi Either String Piket, lalu menyimpan hasil Right sebagai nilai Piket yang sah, sedangkan Left ditangani di batas tersebut. Setelah melewati titik ini, sisa program bekerja dengan tipe Piket yang tidak mungkin berisi data setengah jadi, karena satu-satunya cara menuju tipe itu sudah memeriksa bentuknya.

Pemisahan ini juga menjelaskan peran praktis OverloadedStrings pada instans di atas, yaitu literal seperti "nama" pada instans FromJSON bertipe Key atau Text, bukan String, sehingga penulisan tetap ringkas tanpa kehilangan ketepatan tipe.

Kesalahan umum#

Kesalahan pertama adalah membaca JSON dengan mencocokkan teksnya secara manual, misalnya pencarian potongan string, karena perubahan kecil pada format sumber menghasilkan data yang keliru tanpa peringatan. Pustaka aeson menggantikan pekerjaan itu dengan parser yang kegagalannya dilaporkan sebagai Left, sehingga bentuk yang tidak sesuai tertolak alih-alih menyusup sebagai data rusak.

Kesalahan kedua berupa menganggap decode yang menghasilkan Nothing sebagai kegagalan tanpa sebab. Kembalian Maybe dari decode memang menyembunyikan pesan kegagalannya, sehingga sumber yang bentuknya sering berubah lebih tepat dibaca dengan eitherDecode yang menyertakan pesan tersebut. Kesalahan ketiga berupa menyalin nama field pada instans ToJSON tanpa menyamakannya pada instans FromJSON, kegagalan yang lolos build tetapi gagal pada round-trip, dan pengujian properti bolak-balik itulah yang menutup celahnya.

Ringkasan#

  • Instans ToJSON dan FromJSON menghubungkan tipe proyek dengan JSON, dan nama field JSON dinyatakan eksplisit di kedua arah.
  • encode menghasilkan ByteString, sedangkan eitherDecode mengembalikan Either String a yang memuat nilai atau pesan kegagalannya.
  • Properti decode (encode x) == x menguji perjalanan bolak-balik dan menutup celah field yang terlewat maupun pemangkasan tipe.
  • Data mentah dari luar dibaca dan divalidasi di batas, lalu sisa program bekerja dengan nilai tervalidasi yang tidak perlu diperiksa lagi.

Referensi#