Prezantimi i spray-json
Në Starmind, ne përdorim bibliotekën spray-json për të analizuar dhe gjeneruar JSON. Përcaktimi se si krijohet JSON për klasat tona mund të duket kështu:
import spray.json._ case class Person(firstName: String, lastName: String) implicit val personJsonWriter = new JsonWriter[Person] { def write(person: Person): JsValue = { JsObject( "first_name" -> JsString(person.firstName), "last_name" -> JsString(person.lastName) ) } } Person(firstName = "David", lastName = "Bowie").toJson
"Ekzekutoni këtë kod në Scastie."
JsonWriter
përdoret si një parametër i nënkuptuar për metodën .toJson
. Kjo prodhon një objekt JsValue
(nga DSL që spray-json përdor për të përfaqësuar JSON në Scala) në të cilin për shembull mund të thërrasim .prettyPrint
për ta kthyer atë në një varg të njohur JSON:
{ "first_name": "David", "last_name": "Bowie" }
Ka edhe dy gjëra të tjera që duhet të kuptojmë në lidhje me spray-json për këtë artikull.
Së pari, përveç tiparit JsonWriter[T]
me metodën write
për të kthyer diçka të tipit T
në JsValue
, ekziston edhe një tipar JsonReader[T]
me metodën read
për të bërë konvertimin e kundërt (nga JsValue
në llojin T
). Kur duam të mbështesim leximin dhe shkrimin, ne mund të zbatojmë një JsonFormat[T]
i cili thjesht "përcaktohet si"
trait JsonFormat[T] extends JsonReader[T] with JsonWriter[T]
Së dyti, ka tipare RootJsonWriter
, RootJsonReader
dhe RootJsonFormat
, të cilat shtrihen nga JsonWriter
, JsonReader
dhe JsonFormat
respektivisht, pa shtuar ndonjë funksionalitet shtesë. "Dokumentacioni spray-json" shpjegon:
Sipas specifikimit JSON, jo të gjitha llojet e vlerave të përcaktuara JSON lejohen në nivelin rrënjësor të një dokumenti JSON. Një varg JSON për shembull (si
"foo"
) nuk përbën një dokument ligjor JSON në vetvete. Vetëm objektet JSON ose vargjet JSON lejohen si rrënjë dokumentesh JSON.
Për të dalluar, në nivelin e tipit, JsonFormats "të rregullt" nga ato që prodhojnë objekte ose vargje JSON të nivelit rrënjë spray-json përcakton llojin "
RootJsonFormat
", i cili nuk është gjë tjetër veçse një specializim shënuesi prejJsonFormat
. Bibliotekat që mbështesin spray-json si një mjet për serializimin e dokumenteve mund të zgjedhin të varen nga njëRootJsonFormat[T]
për një lloj të personalizuarT
(në vend se një "i thjeshtë"JsonFormat[T]
), në mënyrë që të mos lejojnë paraqitjen e dokumentit të paligjshëm rrënjët.
Në fakt, ajo që është ose nuk është e vlefshme JSON e nivelit të lartë nuk duket të jetë aq e thjeshtë, me "specifikime të ndryshme që përdorin përkufizime të ndryshme". Megjithatë, kjo është një histori krejtësisht e ndryshme…
JsonWriters që mungojnë
Tani spray.json.DefaultJsonProtocol
(i disponueshëm si trait
dhe si object
) përmban një numër shkrimtarësh, lexuesish dhe formatesh të paracaktuara, si dhe metoda të nënkuptuara që mund p.sh. jep automatikisht një JsonFormat[List[T]]
për çdo lloj T
që ka një JsonFormat[T]
të nënkuptuar. (Blogu i programimit të Haoyi ka një shpjegim të mirë se si funksionojnë këto të nënkuptuara.) Kjo na mundëson të bëjmë diçka të tillë:
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 davidBowie = Person(firstName = "David", lastName = "Bowie") val iggyPop = Person(firstName = "Iggy", lastName = "Pop") List(davidBowie, iggyPop).toJson
"Ekzekutoni këtë kod në Scastie."
Metoda .toJson
kërkon një JsonWriter[List[Person]]
të nënkuptuar, e cila ofrohet nga "metoda"
implicit def listFormat[T :JsonFormat]: RootJsonFormat[List[T]]
nga DefaultJsonProtocol
me parametrin e tipit T
të vendosur në Person
(dhe lidhja e kontekstit :JsonFormat
është e kënaqur nga JsonFormat[Person]
tonë të nënkuptuar). Me fjalë të tjera, duke shkruar më qartë
List(davidBowie, iggyPop).toJson(listFormat(personJsonFormat))
është e barabartë me List(davidBowie, iggyPop).toJson
.
Unë përdora një JsonFormat
këtu në vend të një JsonWriter
. Nëse ne jemi të interesuar vetëm të shkruajmë, do të ishte më e pastër të zbatonim vetëm një JsonWriter
, në vend që ta linim metodën read
të pazbatuar, ose ta bënim atë të bënte një përjashtim. Në mënyrë të bezdisshme, spray-json duket se inkurajon pikërisht këtë të fundit, duke provuar një "metodë"
def lift[T](writer :JsonWriter[T]) = new JsonFormat[T] { def write(obj: T): JsValue = writer.write(obj) def read(value: JsValue) = throw new UnsupportedOperationException("JsonReader implementation missing") }
në DefaultJsonProtocol
, ndërkohë që neglizhohet të ofrojë ndonjë JsonWriter
s të nënkuptuar për listat ose koleksionet e tjera.
Fatmirësisht, ky lëshim në bibliotekën spray-json na ofron mundësinë të mësojmë disa mësime të rëndësishme rreth zgjidhjes së nënkuptuar, duke u përpjekur të zbatojmë vetë JsonWriter
s që mungojnë…
Zgjerimi i Protokollit DefaultJson
Ne në thelb mund të kopjojmë-ngjitim disa rreshta nga kodi burimor spray-json për të krijuar JsonWriter
s të nënkuptuara për listat.
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 davidBowie = Person(firstName = "David", lastName = "Bowie") val iggyPop = Person(firstName = "Iggy", lastName = "Pop") List(davidBowie, iggyPop).toJson
"Ekzekutoni këtë kod në Scastie."
Lehtë.
Por, çka nëse ne, pasi kemi zgjatur kështu protokollin e paracaktuar JSON, vendosim që duam të mbështesim leximin dhe shkrimin për klasën tonë të rastit Person
? Ne mund ta zëvendësojmë JsonWriter[Person]
tonë të nënkuptuar me njëJsonFormat[Person]
të nënkuptuar. Le të provojmë me formatin që spray-json mund të gjenerojë automatikisht për klasat e rasteve:
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 davidBowie = Person(firstName = "David", lastName = "Bowie") val iggyPop = Person(firstName = "Iggy", lastName = "Pop") List(davidBowie, iggyPop).toJson
"Ekzekutoni këtë kod në Scastie."
Papritur përpiluesi nuk do ta pranojë më kodin…
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(davidBowie, iggyPop).toJson ^
Nga mesazhi i gabimit "vlera të paqarta të nënkuptuara", mund të kuptojmë se përpiluesi ka dy mënyra për të gjeneruar në mënyrë implicite një JsonWriter[List]
: duke përdorur DefaultJsonProtocol
për të krijuar një RootJsonFormat[List]
, d.m.th.
List(davidBowie, iggyPop).toJson(listFormat(personJsonFormat))
ose duke përdorur metodën tonë listJsonWriter
për të kthyer JsonFormat[Person]
në RootJsonWriter[List]
:
List(davidBowie, iggyPop).toJson(listJsonWriter(personJsonFormat))
Scala do të ishte goxha e gjymtuar nëse do të bënte gjithmonë një gabim sa herë që zbatohet më shumë se një konvertim i nënkuptuar. Sigurisht që Scala ka disa mekanizma për të renditur konvertimet e nënkuptuara të disponueshme dhe për të zgjedhur atë më të përshtatshëm. Por si funksionon saktësisht kjo dhe pse dështoi në shembullin e mësipërm? Koha për t'u zhytur në "Specifikimet e Gjuhës Scala" të frikshme!
Kuptimi i zgjidhjes së nënkuptuar
Specifikimi i gjuhës Scala ka një kapitull i cili i kushtohet impliciteve, kështu që le të fillojmë të kërkojmë atje. Në seksionin "7.2 Parametrat e nënkuptuar", gjejmë një përkufizim të sferës së nënkuptuar (i cili mund të jetë subjekt i disa eseve në vetvete; "Pyetjet e shpeshta të Scala" është një pikënisje e mirë) si dhe fjali:
Nëse ka disa argumente të pranueshëm që përputhen me llojin e parametrit të nënkuptuar, do të zgjidhet një më specifik duke përdorur rregullat e "rezolucionit të mbingarkesës" statike.
Kjo na çon në seksionin "6.26.3 Rezolucioni i mbingarkesës"", i cili është mjaft i frikshëm për t'u lexuar. Megjithatë, nëse heqim të gjitha detajet teknike, idetë kryesore nuk janë aq komplekse. Scala kërkon alternativën "më specifike". Këtu, të qenit "më specifik" është një marrëdhënie e pjesshme që merr parasysh dy aspekte të ndryshme. Për të vendosur nëse një alternativë X është më specifike se një alternativë Y, mund të imagjinojmë një garë në dy raunde (një raund për secilin nga dy aspektet), ku rezultati përfundimtar është shuma e rezultateve të raundeve individuale.
- Rundi 1: Ku përcaktohet alternativa?
X shënon një pikë mbi Y kur X përcaktohet në një klasë ose objekt që rrjedh nga (për shembull zgjerohet nga) klasa ose objekti ku përcaktohet Y (dhe anasjelltas). - Rundi 2: Cili është lloji i alternativës?
X shënon një pikë mbi Y kur lloji i X "përputhet" me (d.m.th. është një nënlloj i) llojit të Y (dhe anasjelltas).
(Nuk është e qartë për ta nxjerrë këtë nga përkufizimi teknik i dhënë në Specifikimet e Gjuhës Scala, i cili në mënyrë konfuze përdor gjithashtu fjalët "aq specifike sa". Megjithatë, shumica e detajeve teknike kanë të bëjnë me aplikimin e funksionit dhe nuk ndikojnë në rezolucionin e nënkuptuar.)
Nëse ka një fitues pas mbledhjes së rezultateve të dy raundeve, atëherë kjo alternativë është më specifike.
Sidoqoftë, rezultati mund të jetë gjithashtu një barazim, i cili shkakton gabimin e "vlerave të nënkuptuara të paqarta" që kemi hasur më parë:
- Rezultati është barazim në të dyja raundet. Për shembull, të dyja alternativat kanë të njëjtin lloj dhe përcaktohen në të njëjtën klasë/objekt (ose në klasa/objekte të palidhura).
- Një alternativë fiton një raund dhe alternativa tjetër fiton raundin tjetër. Për shembull, alternativa X përcaktohet në një nënklasë ku përcaktohet alternativa Y, por në të njëjtën kohë lloji i Y është një nëntip i tipit X.
Situata e dytë ndodhi në shembullin tonë. listJsonWriter
jonë përkufizohet në një objekt që shtrihet nga DefaultJsonProtocol
ku është përcaktuar listFormat
e spray-json. Pra, raundi 1 është një fitore 1–0 për listJsonWriter
tonë. Megjithatë, JsonFormat[List[T]]
, që është lloji i kthyer nga metoda listFormat
, është një nënlloj i JsonWriter[List[T]]
, lloji i kthyer nga metoda jonë listJsonWriter
. Pra, raundi 2 është një fitore 0–1 për listFormat
të spray-json. Në përgjithësi është një barazim, prandaj përpiluesi Scala hedh një gabim "vlera të paqarta të nënkuptuara".
Gjetja e nënkuptimeve të sakta
Me këtë njohuri se si funksionon zgjidhja e nënkuptuar, zgjidhja për problemin tonë të "vlerave të nënkuptuara ambiciozë" nuk është e vështirë të gjendet. Meqenëse nuk mund të bëjmë shumë për llojin e metodës listJsonWriter
, duhet ta bëjmë këtë metodë më pak specifike se listFormat
e spray-json duke ndryshuar ku është përcaktuar. Nëse ExtendedJsonProtocol
-ja jonë nuk shtrihet më nga DefaultJsonProtocol
, atëherë duhet të jemi mirë. Një mënyrë për ta arritur këtë në fletën tonë të punës është të ndryshojmë extends DefaultJsonProtocol
në import DefaultJsonProtocol._
.
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 davidBowie = Person(firstName = "David", lastName = "Bowie") val iggyPop = Person(firstName = "Iggy", lastName = "Pop") List(davidBowie, iggyPop).toJson
"Ekzekutoni këtë kod në Scastie."
Dhe jo, funksionon përsëri siç pritej! ExtendedJsonProtocol
jonë nuk i pengon më DefaultJsonProtocol
.
Vini re se në shembullin e mësipërm, ExtendedJsonProtocol
nuk përdoret fare. Vetëm nëse një lloj T
ka një JsonWriter
por jo JsonFormat
, atëherë metoda jonë implicitlistJsonWriter
do të përdoret kur nevojitet një JsonWriter[List[T]]
. Sa herë që disponohet një JsonFormat
, në vend të kësaj do të përdoret metoda më specifike listFormat
nga spray-json.
Shpresoj ta keni gjetur të dobishëm këtë artikull. Do të isha i lumtur të dëgjoja ndonjë reagim. Shpresoj të publikoj më shumë artikuj si ky mbi programimin e avancuar Scala në të ardhmen.