Giới Thiệu
- JSON là định dạng khá quen thuộc với bất cứ developer nào khi phải lập trình với API. Đây là một định dạng khá phổ biến, cấu trúc dễ đọc, gọn nhẹ. Các ngôn ngữ lập trình đều có ít nhiều các thư viện hỗ trợ việc Parse hoặc Generate ra JSON. Trong bài viết này, mình xin giới thiệu một thư viện khá gọn nhẹ để xử lý JSON trong Scala, đó là thư viện
spray-json
- spray-json là một thư viện thuộc
Spray Project
- bộ toolkit dành cho việc build REST/HTTP integration layer trên nền tảng Scala và Akka (Xem thêm về Spray tại đây) - Trong một bài viết trước, chúng ta có phần nào khái niệm về
implicit
trong Scala (Xem thêm bài viết đó tại đây). Các bạn có thể đọc tham khảo để hiểu sơ về khái niệmimplicit
trong Scala.
Tính Năng
Ưu điểm
- spray-json hỗ trợ các chức năng:
- Kiểm soát các element trong JSON model.
- Hỗ trợ JSON parser hiệu quả.
- Có thể chọn cách in định dạng JSON ra string dưới dạng pretty hoặc compact.
- Có thể serialization - deserialization một object thành kiểu Class.
- Không cần thêm bất kì dependencies nào.
- spray-json cho phép convert giữa các định dạng
- Chuỗi JSON.
- JSON Abstract Syntax Tree (ASTs) dựa trên JsValue.
- Một instance theo kiểu dữ liệu trong Scala.
Cài đặt
- spray-json có thể cài đặt bằng maven.
- Ngoài ra, nếu dùng SBT để cài đặt bằng cách thêm vào file
build.sbt
:
libraryDependencies += "io.spray" %% "spray-json" % "1.3.4"
Sử Dụng
Cơ bản
Import thư viện
- Để sử dụng thư viện, chúng ta cần import thư viện vào theo cú pháp
import spray.json._
import DefaultJsonProtocol._ #Trường hợp chúng ta không sử dụng Protocol tự định nghĩa
Parse JSON String
- Chúng ta có thể parse JSON string thành định dạng AST như sau
val source = """{ "some": "JSON source" }"""
val jsonAst = source.parseJson // or JsonParser(source)
Convert 1 Scala Object thành JSON AST
- Chúng ta sử dụng extension method
toJson
để convert Scala Object thành JSON AST
val jsonAst = List(1, 2, 3).toJson
Nâng cao
- Ngoài ra, ta có thể convert 1 Scala Object có type
T
thành định dạng JSON AST và ngược lại, convert JSON AST thành Scala Object. Để làm điều này, chúng ta cần phải đem giá trịimplicit
vào trong scope mà nó cung cấpJsonFormat[T]
instance cho typeT
. Ta sẽ làm việc đó thông quaJsonProtocol
spray-json
sử dụngSJSON
để kết nối kiểu dữ liệu typeT
với các logic để serialize - deserialize một instance thành JSON.DefaultJsonProtocol
củaspray-json
bao gồm các implicit values củaJsonFormat[T]
, mỗiJsonFormat[T]
chứa các logic để convert một instance của kiểu dữ liệu typeT
thành JSON hoặc ngược lại.DefaultJsonProtocol
đã cover toàn bộ các kiểu dữ liệu có trong Scala, cho nên đối với các kiểu dữ liệu thông thường, ta có thể dễ dàng convert dữ liệu thành JSON AST với câu lệnhtoJson
. Các kiểu dữ liệu đã được cover bao gồm:- Byte, Short, Int, Long, Float, Double, Char, Unit, Boolean
- String, Symbol
- BigInt, BigDecimal
- Option, Either, Tuple1 - Tuple7
- List, Array
- immutable.{Map, Iterable, Seq, IndexedSeq, LinearSeq, Set, Vector}
- collection.{Iterable, Seq, IndexedSeq, LinearSeq, Set}
- JsValue
Sử dụng DefaultJsonProtocol
- Trong trường hợp chúng ta muốn custom lại type
T
, chúng ta có thể sử dụngDefaultJsonProtocol
vớiJsonFormat[T]
bằng cách sau
case class Student(name: String, age: Int, address: String)
object MyJsonProtocol extends DefaultJsonProtocol {
implicit val studentList = jsonFormat4(Student)
}
import MyJsonProtocol._
import spray.json._
val json = Student("An", 95,"HCM City").toJson
val student = json.convertTo[Student]
Cải Tiến
Tìm hiểu thêm về implicit trong spray-json
Tìm hiểu thêm về JsonWriters
- Với việc hàm
DefaultJsonProtocol
(Có ở dạngtrait
vàobject
) chứa khá nhiều các predefined writers, readers, formats, cũng như là các implicit methods. Check đoạn code dưới đây.
import spray.json._
import spray.json.DefaultJsonProtocol._
case class Person(firstName: String, lastName: String)
implicit val personJsonFormat = new RootJsonFormat[Person] {
def write(person: Person): JsValue = {
JsObject(
"first_name" -> person.firstName.toJson,
"last_name" -> person.lastName.toJson
)
}
def read(value: JsValue): Person = ???
}
val nguyenQuang = Person(firstName = "Nguyen", lastName = "Quang")
val tranTin = Person(firstName = "Tran", lastName = "Tin")
List(nguyenQuang, tranTin).toJson
- Ở đây, hàm
toJson
tìm kiếm một implicitJsonWriter[List[Person]]
được provide bởi hàm trong spray-json
implicit def listFormat[T :JsonFormat]: RootJsonFormat[List[T]]
- Ở đây, type
T
được hiểu làPerson
, và nội dung củaJsonFormat
bị ràng buộc bởi implicitJsonFormat[Person]
mà ta đã định nghĩa ở trên. Nói cách khác, câu lệnh xuất ra Json choList[Person]
có thể viết phức tạp là
#Phức tạp
List(nguyenQuang, tranTin).toJson(listFormat(personJsonFormat))
#Đơn giản
List(nguyenQuang, tranTin).toJson
- Thêm nữa, ở đây chúng ta dùng
JsonFormat
thay choJsonWriter
. Nếu như chúng ta chỉ có nhu cầu xài hàm write (Xuất object ra JSON) thì chúng ta chỉ nên sử dụngJsonWriter
, thay vì không implement hàmread
hoặc throw exception mặc định. TrongDefaultJsonProtocol
của spray-json, phương pháp này được sử dụng khá là phổ biến (Sử dụngJsonFormat
, không implement hàmread
mà chỉ throw exception) mà bỏ qua việc cung cấpJsonWriter
cho list hoặc Collection.
Mở rộng DefaultJsonProtocol
- Như đã trình bày ở trên, chúng ta có thể chỉ cần dùng
JsonWriter
cho việc xuất JSON. Chúng ta có thể làm theo sau
import spray.json._
object ExtendedJsonProtocol extends DefaultJsonProtocol {
implicit def listJsonWriter[T : JsonWriter]: RootJsonWriter[List[T]] = new RootJsonWriter[List[T]] {
def write(list: List[T]): JsArray = JsArray(list.map(_.toJson).toVector)
}
}
import ExtendedJsonProtocol._
case class Person(firstName: String, lastName: String)
implicit val personJsonWriter = new RootJsonWriter[Person] {
def write(person: Person): JsValue = {
JsObject(
"first_name" -> person.firstName.toJson,
"last_name" -> person.lastName.toJson
)
}
}
val nguyenQuang = Person(firstName = "Nguyen", lastName = "Quang")
val tranTin = Person(firstName = "Tran", lastName = "Tin")
List(nguyenQuang, tranTin).toJson
- Đoạn code trên chạy sẽ cho kết quả giống như kết quả của ví dụ trên sử dụng
JsonFormat
. - Tuy nhiên, trong trường hợp chúng ta muốn mở rộng Protocol của riêng mình có thể hỗ trợ cả việc
read
vàwrite
cho lớpPerson
thì phải làm thế nào? Ở đây ta thử thay thế implicitJsonWriter[Person]
bằng implicitJsonFormat[Person]
theo như dưới đây
import spray.json._
object ExtendedJsonProtocol extends DefaultJsonProtocol {
implicit def listJsonWriter[T : JsonWriter]: RootJsonWriter[List[T]] = new RootJsonWriter[List[T]] {
def write(list: List[T]): JsArray = JsArray(list.map(_.toJson).toVector)
}
}
import ExtendedJsonProtocol._
case class Person(firstName: String, lastName: String)
implicit val personJsonFormat = jsonFormat2(Person)
val nguyenQuang = Person(firstName = "Nguyen", lastName = "Quang")
val tranTin = Person(firstName = "Tran", lastName = "Tin")
List(nguyenQuang, tranTin).toJson
- Lúc này, compiler sẽ không chấp nhận code mà báo lỗi
Error:(17, 46) ambiguous implicit values:
both method listFormat in trait CollectionFormats of type [T](implicit evidence$1: spray.json.JsonFormat[T])spray.json.RootJsonFormat[List[T]]{def write(list: List[T]): spray.json.JsArray}
and method listJsonWriter in object ExtendedJsonProtocol of type [T](implicit evidence$1: spray.json.JsonWriter[T])spray.json.RootJsonWriter[List[T]]
match expected type spray.json.JsonWriter[List[A$A10.this.Person]]
lazy val result = List(nguyenQuang, tranTin).toJson
- Từ lỗi "ambigious implicit values", ta có thể đoán là nguyên nhân do compiler đang không biết chọn cách nào để chọn đúng giá trị implicit cho
JsonWriter[List]
, cụ thể ở đây là-
- Sử dụng
DefaultJsonProtocol
để tạoRootJsonFormat[List]
- Sử dụng
List(nguyenQuang, tranTin).toJson.toJson(listFormat(personJsonFormat))
-
- Sử dụng method
listJsonWriter
mà ta định nghĩa để chuyển đổiJsonFormat[Person]
thànhRootJsonWriter[List]
- Sử dụng method
List(nguyenQuang, tranTin).toJson(listJsonWriter(personJsonFormat))
-
Tìm hiểu về lỗi "ambigious implicit values"
- Lỗi "ambigious implicit values" xảy ra là do Scala đang không biết tìm giá trị implicit phù hợp để thay thế vào vị trí. Bản thân Scala có một hệ thống để đánh rank cho các implicit hiện có và chọn ra cái phù hợp nhất. Theo như tài liệu của Scala, mục "7.2 Implicit parameters, ta có thể tìm thấy cụm từ implicit scope cùng với một chú ý.
If there are several eligible arguments which match the implicit parameter’s type, a most specific one will be chosen using the rules of static overloading resolution.
-
Tóm gọn lại 6.26.3 Overloading resolution, ý tưởng chính là Scala tìm kiếm một alternative phù hợp nhất với implicit. Và để xác định một alternative X có phù hợp hơn alternative Y hay không, ta có thể tưởng tượng là đó là một trận đấu gồm 2 vòng, kết quả cuối cùng là tổng điểm cộng lại.
- Vòng 1: alternative được định nghĩa ở đâu?
- X sẽ hơn Y 1 điểm nếu như X được định nghĩa trong class hoặc object có nguồn gốc (ví dụ là extend from) từ class hoặc object mà Y được định nghĩa (và ngược lại).
- Vòng 2: alternative thuộc loại gì
- X sẽ hơn Y 1 điểm nếu type của X comform (ví dụ như là subtype) của Y.
- Vòng 1: alternative được định nghĩa ở đâu?
-
Kết quả cuối cùng sẽ là tổng điểm của 2 vòng. Tuy nhiên, trường hợp Hoà vẫn có thể xảy ra, ví dụ như
- Cả 2 alternative hoà ở cả 2 ván. Ví dụ như cả alternative có cùng type và đều cùng được định nghĩa trong cùng class/object
- Mỗi alternative thắng 1 vòng. Ví dụ X được định nghĩa trong 1 subclass mà Y được định nghĩa, nhưng type của Y lại là subtype của X.
-
Trường hợp 2 xảy ra cho ví dụ ở trên
listJsonWriter
được định nghĩa trong object mà nó extend từDefaultJsonProtocol
nơilistFormat
của spray-json được định nghĩa. Do đó 1-0 nghiêng về phía ta.JsonFormat[List[T]
- type được return về từ methodlistFormat
lại là subtype củaJsonWriter[List[T]
, type này được return về từ methodlistJsonWriter
của ta định nghĩa. Do đó, tỉ số hiệp 2 là 0-1.
-
Tổng hợp kết quả trên, ta có 1 kết quả hoà, và gây ra lỗi "ambigious implicit values" như trên.
Giải pháp
- Từ những kết quả tìm hiểu được như trên, ta có thể thấy có 2 cách giải quyết vấn đề này: hoặc là thay đổi nơi định nghĩa của
listJsonWriter
, hoặc là thay đổi type được trả về từlistJsonWriter
. - Tuy nhiên, vì hàm
listJsonWriter
khá là khó thay đổi, nên cách khả dĩ nhất là thay đổi nơi định nghĩa của nó, làm nó bớt specific hơn hàmlistFormat
của spray-json. Một cách để thực hiện việc này là thay đổiextends DefaultJsonProtocol
thànhimport DefaultJsonProtocol._
như dưới đây:
import spray.json._
import DefaultJsonProtocol._
object ExtendedJsonProtocol {
implicit def listJsonWriter[T : JsonWriter]: RootJsonWriter[List[T]] = new RootJsonWriter[List[T]] {
def write(list: List[T]): JsArray = JsArray(list.map(_.toJson).toVector)
}
}
import ExtendedJsonProtocol._
case class Person(firstName: String, lastName: String)
implicit val personJsonFormat = jsonFormat2(Person)
val nguyenQuang = Person(firstName = "Nguyen", lastName = "Quang")
val tranTin = Person(firstName = "Tran", lastName = "Tin")
List(nguyenQuang, tranTin).toJson
Kết luận
- spray-json là một thư viện khá tốt, gọn nhẹ, phục vụ tốt cho việc xử lý JSON. Ngoài ra, với các hiểu biết về implicit, ta cũng có thể dễ dàng custom lại Protocol theo ý bản thân để phù hợp hơn cho dự án.