intro.bbdoc 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. ## Serialising with JConv
  2. In the context of #Text.JConv, serialisation is the mapping of BlitzMax objects to their JSON representation.
  3. Take the following `TUser` type to start with :
  4. ```blitzmax
  5. Type TUser
  6. Field name:String
  7. Field email:String
  8. Field age:int
  9. End Type
  10. ```
  11. The `TUser` object has three Fields,
  12. * The user's `name` is a #String
  13. * The user's `email` is a #String
  14. * The user's `age` is an #Int
  15. An application needs to convert a 'TUser' into its JSON representation, so assuming the member names
  16. remained the same, we could expect a typical JSON representation to look like this :
  17. ```json
  18. {
  19. "name" : "bob",
  20. "email" : "[email protected]",
  21. "age" : 30
  22. }
  23. ```
  24. To convert a `TUser` to JSON, we first construct an instance of one for the user Bob :
  25. ```blitzmax
  26. Local user:TUser = New TUser("bob", "[email protected]", 30)
  27. ```
  28. In order to do the serialisation, we need an instance of #TJConv to do the conversion :
  29. ```blitzmax
  30. Local jconv:TJConv = New TJConvBuilder.Build()
  31. ```
  32. The next step is to call the #ToJson method of #TJConv, passing the object we want to serialise :
  33. ```blitzmax
  34. Local json:String = jconv.ToJson(user)
  35. ```
  36. The `json` #String contains the following value :
  37. ```json
  38. {"name": "bob", "email": "[email protected]", "age": 30}
  39. ```
  40. Notice that #Text.JConv respects the field types, wrapping Strings in quotes, but not so for numbers.
  41. Just a single method call is required to do the conversion of the entire object. This is useful when
  42. working with more complex object structures.
  43. Here's the example in full :
  44. ```blitzmax
  45. SuperStrict
  46. Framework BRL.StandardIO
  47. Import Text.JConv
  48. Local user:TUser = New TUser("bob", "[email protected]", 30)
  49. Local jconv:TJConv = New TJConvBuilder.Build()
  50. Local json:String = jconv.ToJson(user)
  51. Print json
  52. Type TUser
  53. Field name:String
  54. Field email:String
  55. Field age:Int
  56. Method New(name:String, email:String, age:Int)
  57. Self.name = name
  58. Self.email = email
  59. Self.age = age
  60. End Method
  61. End Type
  62. ```
  63. ## Deserialising with JConv
  64. We'll start by creating a #String containing the JSON to convert :
  65. ```blitzmax
  66. Local json:String = "{~qname~q: ~qbob~q, ~qemail~q: [email protected]~q, ~qage~q: 30}"
  67. ```
  68. Again, we'll build an instance of #TJConv which will perform the conversion :
  69. ```blitzmax
  70. Local jconv:TJConv = New TJConvBuilder.Build()
  71. ```
  72. Finally, we need to map the JSON to a BlitzMax #Object with #FromJson :
  73. ```blitzmax
  74. Local user:TUser = TUser(jconv.FromJson(json, "TUser"))
  75. ```
  76. Note that the second argument specifies the name of the #Type we want the #String to map the JSON to.
  77. Without this *hint*, #Text.JConv wouldn't know what #Type to create from the text.
  78. The `user` object returned from #FromJson will have its fields populated accordingly.
  79. Here's the example in full :
  80. ```blitzmax
  81. SuperStrict
  82. Framework BRL.StandardIO
  83. Import Text.JConv
  84. Local json:String = "{~qname~q: ~qbob~q, ~qemail~q: [email protected]~q, ~qage~q: 30}"
  85. Local jconv:TJConv = New TJConvBuilder.Build()
  86. Local user:TUser = TUser(jconv.FromJson(json, "TUser"))
  87. Print "name = " + user.name
  88. Print "email = " + user.email
  89. Print "age = " + user.age
  90. Type TUser
  91. Field name:String
  92. Field email:String
  93. Field age:Int
  94. Method New(name:String, email:String, age:Int)
  95. Self.name = name
  96. Self.email = email
  97. Self.age = age
  98. End Method
  99. End Type
  100. ```
  101. ## Serialising Nested Objects
  102. #Text.JConv can also handle the conversion of more complex objects that include the nesting of other non-primitive objects.
  103. To demostrate this we will extend the `TUser` type to include an address, which will be represented by the `TAddress` #Type :
  104. ```blitzmax
  105. Type TUser
  106. Field name:String
  107. Field email:String
  108. Field age:Int
  109. Field address:TAddress
  110. End Type
  111. Type TAddress
  112. Field line1:String
  113. Field city:String
  114. Field country:String
  115. End Type
  116. ```
  117. In BlitzMax the two models are cleanly separated by types, and the `TAddress` reference is held in the `address` #Field of the user.
  118. In JSON however, the address must be nested directly within the user object, as we can see here :
  119. ```json
  120. {
  121. "name" : "bob",
  122. "email" : "[email protected]",
  123. "age" : 30,
  124. "address" : {
  125. "line1" : "66 Some Street",
  126. "city" : "Someville",
  127. "country" : "Someland"
  128. }
  129. }
  130. ```
  131. We'll initially create the required BlitzMax objects :
  132. ```blitzmax
  133. Local address:TAddress = New TAddress("66 Some Street", "Someville", "Someland")
  134. Local user:TUser = New TUser("bob", "[email protected]", 30, address)
  135. ```
  136. And then serialise the user with an instance of #TJConv :
  137. ```blitzmax
  138. Local jconv:TJConv = New TJConvBuilder.Build()
  139. Local json:String = jconv.ToJson(user)
  140. ```
  141. The resulting conversion to JSON is :
  142. ```json
  143. {"name": "bob", "email": "[email protected]", "age": 30, "address": {"line1": "66 Some Street", "city": "Someville", "country": "Someland"}}
  144. ```
  145. As you can see, #Text.JConv has correctly nested the address inside the user as a JSON object.
  146. ## Deserialising Nested Objects
  147. In the real world, the developer is often presented with a JSON API from which they need to construct the relevant BlitzMax
  148. Types in order to import the data.
  149. As we have seen previously, the structure of a JSON object maps relatively well to a BlitzMax #Object and its fields.
  150. In the next example, we'll start with a JSON object and construct a set of BlitzMax Types that we can use to deserialise the
  151. data in order to use it within our BlitzMax application.
  152. In this particular example, we will be retrieving some airport information from an online flight information resource :
  153. ```json
  154. {
  155. "id": "BER",
  156. "code": "BER",
  157. "name": "Berlin Brandenburg",
  158. "slug": "berlin-brandenburg-berlin-germany",
  159. "timezone": "Europe/Berlin",
  160. "city": {
  161. "id": "berlin_de",
  162. "name": "Berlin",
  163. "code": "BER",
  164. "slug": "berlin-germany",
  165. "country": {
  166. "id": "DE",
  167. "name": "Germany",
  168. "slug": "germany",
  169. "code": "DE"
  170. },
  171. "region": {
  172. "id": "central-europe",
  173. "name": "Central Europe",
  174. "slug": "central-europe"
  175. },
  176. "continent": {
  177. "id": "europe",
  178. "name": "Europe",
  179. "slug": "europe",
  180. "code": "EU"
  181. }
  182. },
  183. "location": {
  184. "lat": 52.366667,
  185. "lon": 13.503333
  186. }
  187. }
  188. ```
  189. As you can see, there are several levels of nesting. The main airport object has a nested `city`, and within that `country`, `region` and `continent` objects.
  190. Also notice that many of the objects share a similar structure (`id`, `name`, `slug`, and `code`).
  191. We can use BlitzMax's #Type polymorphism by creating a base #Type and avoid a lot of duplication :
  192. ```blitzmax
  193. Type TBase
  194. Field id:String
  195. Field code:String
  196. Field name:String
  197. Field slug:String
  198. End Type
  199. ```
  200. Next, we'll define the BlitzMax types that will contain the more specific information :
  201. ```blitzmax
  202. Type TAirport Extends TBase
  203. Field timezone:String
  204. Field city:TCity
  205. Field location:TLocation
  206. End Type
  207. Type TCity Extends TBase
  208. Field country:TCountry
  209. Field region:TRegion
  210. Field continent:TContinent
  211. End Type
  212. Type TCountry Extends TBase
  213. End Type
  214. Type TRegion Extends TBase
  215. End Type
  216. Type TContinent Extends TBase
  217. End Type
  218. Type TLocation
  219. Field lat:Double
  220. Field lon:Double
  221. End Type
  222. ```
  223. Each #Type represents a particular JSON object from original nested JSON. `TCity` contains fields for `country`, `region` and `continent`, each of which are types
  224. representing that particular piece of information.
  225. Where the naming of your fields must match those of the JSON objects, how you name your types is not important for JSON mapping, but you'll generally give them a
  226. name that reflects the kind of information they contain.
  227. Finally, we can use these types to de-serialise a matching JSON object, as shown in this complete example :
  228. ```blitzmax
  229. SuperStrict
  230. Framework BRL.StandardIO
  231. Import Text.JConv
  232. Local data:String = "{~qid~q:~qBER~q,~qcode~q:~qBER~q,~qname~q:~qBerlin Brandenburg~q,~qslug~q:~qberlin-brandenburg-berlin-germany~q,~qtimezone~q:~qEurope/Berlin~q,~qcity~q:{~qid~q:~qberlin_de~q,~qname~q:~qBerlin~q,~qcode~q:~qBER~q,~qslug~q:~qberlin-germany~q,~qcountry~q:{~qid~q:~qDE~q,~qname~q:~qGermany~q,~qslug~q:~qgermany~q,~qcode~q:~qDE~q},~qregion~q:{~qid~q:~qcentral-europe~q,~qname~q:~qCentral Europe~q,~qslug~q:~qcentral-europe~q},~qcontinent~q:{~qid~q:~qeurope~q,~qname~q:~qEurope~q,~qslug~q:~qeurope~q,~qcode~q:~qEU~q}},~qlocation~q:{~qlat~q:52.366667,~qlon~q:13.503333}}"
  233. Local jconv:TJConv = New TJConvBuilder.Build()
  234. Local airport:TAirport = TAirport(jconv.FromJson(data, "TAirport"))
  235. Print "Airport : " + airport.name
  236. Print " City : " + airport.city.name
  237. Print " Location : " + airport.location.lat + ", " + airport.location.lon
  238. Type TBase
  239. Field id:String
  240. Field code:String
  241. Field name:String
  242. Field slug:String
  243. End Type
  244. Type TAirport Extends TBase
  245. Field timezone:String
  246. Field city:TCity
  247. Field location:TLocation
  248. End Type
  249. Type TCity Extends TBase
  250. Field country:TCountry
  251. Field region:TRegion
  252. Field continent:TContinent
  253. End Type
  254. Type TCountry Extends TBase
  255. End Type
  256. Type TRegion Extends TBase
  257. End Type
  258. Type TContinent Extends TBase
  259. End Type
  260. Type TLocation
  261. Field lat:Double
  262. Field lon:Double
  263. End Type
  264. ```
  265. ## Customising Field Names
  266. Occasionally, a JSON object will use a key that has the same name as a reserved keyword in BlitzMax. In that case, you are unable create a field
  267. using the desired name. Fortunately, #Text.JConv allows you use metadata to specify the serialised name of a given field using the `serializedName`
  268. metadata property.
  269. Take the following JSON object as an example :
  270. ```json
  271. {
  272. "field" : "hello",
  273. "for" : "ever"
  274. }
  275. ```
  276. Neither `field` nor `for` are valid names for fields, but we can use the `serializedName` feature to create a valid BlitzMax #Type that can
  277. deserialise this object :
  278. ```blitzmax
  279. Type TCustomFields
  280. Field field_:String { serializedName="field" }
  281. Field anotherField:String { serializedName = "for" }
  282. End Type
  283. ```
  284. As this example demonstrates, when using the `serializedName` metadata property, you can give any name to your fields and the data will still be mapped from
  285. the JSON object correctly.
  286. Here the example in full :
  287. ```blitzmax:
  288. SuperStrict
  289. Framework BRL.StandardIO
  290. Import Text.JConv
  291. Local data:String = "{~qfield~q:~qhello~q,~qfor~q:~qever~q}"
  292. Local jconv:TJConv = New TJConvBuilder.Build()
  293. Local custom:TCustomFields = TCustomFields(jconv.FromJson(data, "TCustomFields"))
  294. Print custom.field_
  295. Print custom.anotherField
  296. Type TCustomFields
  297. Field field_:String { serializedName="field" }
  298. Field anotherField:String { serializedName = "for" }
  299. End Type
  300. ```
  301. In addition to `serializedName`, another metadata property is available during deserialisation, `alternateName`. If you consider `serializedName` as being
  302. the default value, `alternateName` allows you to map other JSON keys to a particular field.
  303. For example, given a `TUser` object where we are already mapping the JSON key `full_name` to the field `name` :
  304. ```blitzmax
  305. Type TUser
  306. Field name:String { serializedName = "full_name" }
  307. Field email:String
  308. Field age:int
  309. End Type
  310. ```
  311. We decide we also want ingest similar data from another system in our application. Instead of `full_name`, the other system uses
  312. `username` for this value. Using the `alternateName` metadata property we can add a comma-delimited list of other names, and our #Type becomes :
  313. ```blitzmax
  314. Type TUser
  315. Field name:String { serializedName = "full_name", alternateName ="username" }
  316. Field email:String
  317. Field age:int
  318. End Type
  319. ```
  320. `alternateName` is only available during deserialisation. #Text.JConv will use either the #Field name or the `serializedName` when mapping a
  321. BlitzMax object to JSON.
  322. The following two sets of JSON would map to a `TUser` object and set the `name` #Field appropriately :
  323. ```json
  324. {
  325. "full_name" : "Bob",
  326. "email" : "[email protected]"
  327. }
  328. ```
  329. ```json
  330. {
  331. "username" : "userBob",
  332. "email" : "[email protected]"
  333. }
  334. ```
  335. If there are multiple fields in the JSON that match, #Text.JConv will apply the value that is processed last. So, in the following example,
  336. deserialising the JSON would result in the `name` #Field containing the value `userBob` :
  337. ```json
  338. {
  339. "full_name" : "Bob",
  340. "username" : "userBob",
  341. "email" : "[email protected]"
  342. }
  343. ```
  344. ## Ignoring Fields
  345. If you don't want a field to be mapped to or from JSON there are some metadata properties that you can apply to your types in order to do so.
  346. The first, `transient`, completely disables field from mapping in either direction.
  347. If you want more finer grained control, the metadata properties `noSerialize` and `noDeserialize` can be used instead.
  348. The `noSerialize` property instructs #Text.JConv not to serialize a particular field to JSON, but it allows data from a JSON object to be
  349. deserialized into the #Field.
  350. On the other hand, `noDeserialize` prevents data from a JSON object from deserializing into the #Field, but does allow it to be serialized into
  351. a JSON object.
  352. We'll apply some properties to the `TUser` object to demonstrate the options :
  353. ```blitzmax
  354. Type TUser
  355. Field name:String
  356. Field email:String { noSerialize }
  357. Field age:int { noDeserialize }
  358. Field passwordHash:String { transient }
  359. End Type
  360. ```
  361. Based on the above example, when serializing an instance of `TUser`, only the `name` and `age` fields would be mapped to JSON.
  362. Similarly, only the `name` and `email` fields would be mapped from a JSON object.
  363. The following is a complete example of these properties in action :
  364. ```blitzmax
  365. SuperStrict
  366. Framework BRL.StandardIO
  367. Import Text.JConv
  368. Local user:TUser = New TUser("bob", "[email protected]", 30, "xxxx")
  369. Local jconv:TJConv = New TJConvBuilder.Build()
  370. Local json:String = jconv.ToJson(user)
  371. Print "json : " + json
  372. json = "{~qname~q: ~qbob~q, ~qemail~q: [email protected]~q, ~qage~q: 30, ~qpasswordHash~q: ~qxxxx~q}"
  373. user = TUser(jconv.FromJson(json, "TUser"))
  374. Print "name : " + user.name
  375. Print "email : " + user.email
  376. Print "age : " + user.age
  377. Print "hash : " + user.passwordHash
  378. Type TUser
  379. Field name:String
  380. Field email:String { noSerialize }
  381. Field age:Int { noDeserialize }
  382. Field passwordHash:String { transient }
  383. Method New(name:String, email:String, age:Int, ph:String)
  384. Self.name = name
  385. Self.email = email
  386. Self.age = age
  387. Self.passwordHash = ph
  388. End Method
  389. End Type
  390. ```
  391. ## Configuring TJConv with the Builder
  392. You may have noticed, that by default #Text.JConv serialises the JSON into a single line.
  393. You can change this behaviour with one ofthe builder's configurable options.
  394. The builder uses what is known as a fluent interface, or method chaining design, where a sequence of method calls can be used to construct the #TJConv instance.
  395. For example, the following builder creates an instance of #TJConv which will serialise objects to JSON with a decimal a precision of 2 places and compact objects :
  396. ```blitzmax
  397. Local jconv:TJConv = New TJConvBuilder.WithPrecision(2).WithCompact().Build()
  398. ```
  399. ### WithIndent
  400. The #WithIndent method of #TJConvBuilder specifies the number of spaces to use for indenting of nested objects. The default of 0 (zero)
  401. means not to use pretty-printing.
  402. This is an example of `TUser` using the default options :
  403. ```json
  404. {"name": "Bob", "email": "[email protected]", "age": 30}
  405. ```
  406. And this is an example of building with #WithIndent :
  407. ```json
  408. {
  409. "name": "Bob",
  410. "email": "[email protected]",
  411. "age": 30
  412. }
  413. ```
  414. ### WithCompact
  415. On the other hand, JSON can be compacted further using the #WithCompact method, which works to remove extra spaces :
  416. ```json
  417. {"name":"Bob","email":"[email protected]","age":30}
  418. ```
  419. ### WithPrecision
  420. The representation of decimal numbers can be controlled by the #WithPrecision method, which specifies the maximum number of decimal places to used.
  421. For example, the default representation of a #Type `TPoint` :
  422. ```blitzmax
  423. Type TPoint
  424. Field x:Double
  425. Field y:Double
  426. End Type
  427. ```
  428. would normally result in the following JSON with fields of the values (10.565, 15.912) :
  429. ```json
  430. {"x": 10.565, "y": 15.912000000000001}
  431. ```
  432. Using a maximum precision of 3 (`WithPrecision(3)`), the resulting JSON would become :
  433. ```json
  434. {"x": 10.565, "y": 15.912}
  435. ```
  436. ### WithEmptyArrays
  437. By default, #Null/empty arrays are not serialised at all. That is, the field is not included in the JSON object.
  438. The #WithEmptyArrays option can be enabled to generate an empty array (`[]`] instead.
  439. ### WithBoxing
  440. Primitive numbers, by their very nature in BlitzMax, have no concept of nullability. JSON, conversely, can represent any field as a null value,
  441. either by simply not including it in the object, or by having the value `null`.
  442. To support this, #Text.JConv provides an option to use "boxed" primitives in your types. A Boxed primitive is just an instance of a #Type that has
  443. a value field of the appropriate numeric #Type. Using a boxed primitive then allows a field to contain a value, or be #Null.
  444. This feature is enabled by using the #WithBoxing option of the builder.
  445. As an example, suppose there is a JSON object which has a numeric field `failures`. The schema specifies that this value can either be `null` or have a value
  446. greater than zero :
  447. ```json
  448. [
  449. {
  450. "jobId": "ABC123",
  451. "failures": 3,
  452. "lastError": "overflow"
  453. },
  454. {
  455. "jobId": "DEF456"
  456. }
  457. ]
  458. ```
  459. Deserialising this wouldn't be a problem, as our `TJob` #Type could represent no `failures` by the number zero :
  460. ```blitzmax
  461. Type TJob
  462. Field jobId:String
  463. Field failures:Int
  464. Field lastError:String
  465. End Type
  466. ```
  467. However, were we required to serialise our #Type to JSON for use by the API, we'd potentially fail schema validation by passing zero as a value for
  468. the `failures` #Field.
  469. Utilising the boxing feature, we could instead define the `failures` #Field as `TInt` :
  470. ```blitzmax
  471. Type TJob
  472. Field jobId:String
  473. Field failures:TInt
  474. Field lastError:String
  475. End Type
  476. ```
  477. Which would, for #Null values, result in the `features` #Field not being serialized to JSON.
  478. Here's a full example highlighting the use of boxing :
  479. ```blitzmax
  480. SuperStrict
  481. Framework BRL.StandardIO
  482. Import Text.JConv
  483. Local job1:TJob = New TJob("ABC123", 3, "overflow")
  484. Local job2:TBoxedJob = New TBoxedJob("DEF456", 0, Null)
  485. Local jconv:TJConv = New TJConvBuilder.WithBoxing().WithIndent(2).Build()
  486. Print jconv.ToJson(job1)
  487. Print jconv.ToJson(job2)
  488. Type TJob
  489. Field jobId:String
  490. Field failures:Int
  491. Field lastError:String
  492. Method New(jobId:String, failures:Int, lastError:String)
  493. Self.jobId = jobId
  494. Self.failures = failures
  495. Self.lastError = lastError
  496. End Method
  497. End Type
  498. Type TBoxedJob
  499. Field jobId:String
  500. Field failures:TInt
  501. Field lastError:String
  502. Method New(jobId:String, failures:Int, lastError:String)
  503. Self.jobId = jobId
  504. If failures > 0 Then
  505. Self.failures = New TInt(failures)
  506. End If
  507. Self.lastError = lastError
  508. End Method
  509. End Type
  510. ```
  511. Running the above example would result in the following output :
  512. ```
  513. {
  514. "jobId": "ABC123",
  515. "failures": 3,
  516. "lastError": "overflow"
  517. }
  518. {
  519. "jobId": "DEF456"
  520. }
  521. ```
  522. ### RegisterSerializer