123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630 |
- ## Serialising with JConv
- In the context of #Text.JConv, serialisation is the mapping of BlitzMax objects to their JSON representation.
- Take the following `TUser` type to start with :
- ```blitzmax
- Type TUser
- Field name:String
- Field email:String
- Field age:int
- End Type
- ```
- The `TUser` object has three Fields,
- * The user's `name` is a #String
- * The user's `email` is a #String
- * The user's `age` is an #Int
- An application needs to convert a 'TUser' into its JSON representation, so assuming the member names
- remained the same, we could expect a typical JSON representation to look like this :
- ```json
- {
- "name" : "bob",
- "email" : "[email protected]",
- "age" : 30
- }
- ```
- To convert a `TUser` to JSON, we first construct an instance of one for the user Bob :
- ```blitzmax
- Local user:TUser = New TUser("bob", "[email protected]", 30)
- ```
- In order to do the serialisation, we need an instance of #TJConv to do the conversion :
- ```blitzmax
- Local jconv:TJConv = New TJConvBuilder.Build()
- ```
- The next step is to call the #ToJson method of #TJConv, passing the object we want to serialise :
- ```blitzmax
- Local json:String = jconv.ToJson(user)
- ```
- The `json` #String contains the following value :
- ```json
- {"name": "bob", "email": "[email protected]", "age": 30}
- ```
- Notice that #Text.JConv respects the field types, wrapping Strings in quotes, but not so for numbers.
- Just a single method call is required to do the conversion of the entire object. This is useful when
- working with more complex object structures.
- Here's the example in full :
- ```blitzmax
- SuperStrict
- Framework BRL.StandardIO
- Import Text.JConv
- Local user:TUser = New TUser("bob", "[email protected]", 30)
- Local jconv:TJConv = New TJConvBuilder.Build()
- Local json:String = jconv.ToJson(user)
- Print json
- Type TUser
- Field name:String
- Field email:String
- Field age:Int
-
- Method New(name:String, email:String, age:Int)
- Self.name = name
- Self.email = email
- Self.age = age
- End Method
- End Type
- ```
- ## Deserialising with JConv
- We'll start by creating a #String containing the JSON to convert :
- ```blitzmax
- Local json:String = "{~qname~q: ~qbob~q, ~qemail~q: [email protected]~q, ~qage~q: 30}"
- ```
- Again, we'll build an instance of #TJConv which will perform the conversion :
- ```blitzmax
- Local jconv:TJConv = New TJConvBuilder.Build()
- ```
- Finally, we need to map the JSON to a BlitzMax #Object with #FromJson :
- ```blitzmax
- Local user:TUser = TUser(jconv.FromJson(json, "TUser"))
- ```
- Note that the second argument specifies the name of the #Type we want the #String to map the JSON to.
- Without this *hint*, #Text.JConv wouldn't know what #Type to create from the text.
- The `user` object returned from #FromJson will have its fields populated accordingly.
- Here's the example in full :
- ```blitzmax
- SuperStrict
- Framework BRL.StandardIO
- Import Text.JConv
- Local json:String = "{~qname~q: ~qbob~q, ~qemail~q: [email protected]~q, ~qage~q: 30}"
- Local jconv:TJConv = New TJConvBuilder.Build()
- Local user:TUser = TUser(jconv.FromJson(json, "TUser"))
- Print "name = " + user.name
- Print "email = " + user.email
- Print "age = " + user.age
- Type TUser
- Field name:String
- Field email:String
- Field age:Int
-
- Method New(name:String, email:String, age:Int)
- Self.name = name
- Self.email = email
- Self.age = age
- End Method
- End Type
- ```
- ## Serialising Nested Objects
- #Text.JConv can also handle the conversion of more complex objects that include the nesting of other non-primitive objects.
- To demostrate this we will extend the `TUser` type to include an address, which will be represented by the `TAddress` #Type :
- ```blitzmax
- Type TUser
- Field name:String
- Field email:String
- Field age:Int
- Field address:TAddress
- End Type
- Type TAddress
- Field line1:String
- Field city:String
- Field country:String
- End Type
- ```
- In BlitzMax the two models are cleanly separated by types, and the `TAddress` reference is held in the `address` #Field of the user.
- In JSON however, the address must be nested directly within the user object, as we can see here :
- ```json
- {
- "name" : "bob",
- "email" : "[email protected]",
- "age" : 30,
- "address" : {
- "line1" : "66 Some Street",
- "city" : "Someville",
- "country" : "Someland"
- }
- }
- ```
- We'll initially create the required BlitzMax objects :
- ```blitzmax
- Local address:TAddress = New TAddress("66 Some Street", "Someville", "Someland")
- Local user:TUser = New TUser("bob", "[email protected]", 30, address)
- ```
- And then serialise the user with an instance of #TJConv :
- ```blitzmax
- Local jconv:TJConv = New TJConvBuilder.Build()
- Local json:String = jconv.ToJson(user)
- ```
- The resulting conversion to JSON is :
- ```json
- {"name": "bob", "email": "[email protected]", "age": 30, "address": {"line1": "66 Some Street", "city": "Someville", "country": "Someland"}}
- ```
- As you can see, #Text.JConv has correctly nested the address inside the user as a JSON object.
- ## Deserialising Nested Objects
- In the real world, the developer is often presented with a JSON API from which they need to construct the relevant BlitzMax
- Types in order to import the data.
- As we have seen previously, the structure of a JSON object maps relatively well to a BlitzMax #Object and its fields.
- 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
- data in order to use it within our BlitzMax application.
- In this particular example, we will be retrieving some airport information from an online flight information resource :
- ```json
- {
- "id": "BER",
- "code": "BER",
- "name": "Berlin Brandenburg",
- "slug": "berlin-brandenburg-berlin-germany",
- "timezone": "Europe/Berlin",
- "city": {
- "id": "berlin_de",
- "name": "Berlin",
- "code": "BER",
- "slug": "berlin-germany",
- "country": {
- "id": "DE",
- "name": "Germany",
- "slug": "germany",
- "code": "DE"
- },
- "region": {
- "id": "central-europe",
- "name": "Central Europe",
- "slug": "central-europe"
- },
- "continent": {
- "id": "europe",
- "name": "Europe",
- "slug": "europe",
- "code": "EU"
- }
- },
- "location": {
- "lat": 52.366667,
- "lon": 13.503333
- }
- }
- ```
- 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.
- Also notice that many of the objects share a similar structure (`id`, `name`, `slug`, and `code`).
- We can use BlitzMax's #Type polymorphism by creating a base #Type and avoid a lot of duplication :
- ```blitzmax
- Type TBase
- Field id:String
- Field code:String
- Field name:String
- Field slug:String
- End Type
- ```
- Next, we'll define the BlitzMax types that will contain the more specific information :
- ```blitzmax
- Type TAirport Extends TBase
- Field timezone:String
- Field city:TCity
- Field location:TLocation
- End Type
- Type TCity Extends TBase
- Field country:TCountry
- Field region:TRegion
- Field continent:TContinent
- End Type
- Type TCountry Extends TBase
- End Type
- Type TRegion Extends TBase
- End Type
- Type TContinent Extends TBase
- End Type
- Type TLocation
- Field lat:Double
- Field lon:Double
- End Type
- ```
- Each #Type represents a particular JSON object from original nested JSON. `TCity` contains fields for `country`, `region` and `continent`, each of which are types
- representing that particular piece of information.
- 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
- name that reflects the kind of information they contain.
- Finally, we can use these types to de-serialise a matching JSON object, as shown in this complete example :
- ```blitzmax
- SuperStrict
- Framework BRL.StandardIO
- Import Text.JConv
- 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}}"
- Local jconv:TJConv = New TJConvBuilder.Build()
- Local airport:TAirport = TAirport(jconv.FromJson(data, "TAirport"))
- Print "Airport : " + airport.name
- Print " City : " + airport.city.name
- Print " Location : " + airport.location.lat + ", " + airport.location.lon
- Type TBase
- Field id:String
- Field code:String
- Field name:String
- Field slug:String
- End Type
- Type TAirport Extends TBase
- Field timezone:String
- Field city:TCity
- Field location:TLocation
- End Type
- Type TCity Extends TBase
- Field country:TCountry
- Field region:TRegion
- Field continent:TContinent
- End Type
- Type TCountry Extends TBase
- End Type
- Type TRegion Extends TBase
- End Type
- Type TContinent Extends TBase
- End Type
- Type TLocation
- Field lat:Double
- Field lon:Double
- End Type
- ```
- ## Customising Field Names
- 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
- using the desired name. Fortunately, #Text.JConv allows you use metadata to specify the serialised name of a given field using the `serializedName`
- metadata property.
- Take the following JSON object as an example :
- ```json
- {
- "field" : "hello",
- "for" : "ever"
- }
- ```
- Neither `field` nor `for` are valid names for fields, but we can use the `serializedName` feature to create a valid BlitzMax #Type that can
- deserialise this object :
- ```blitzmax
- Type TCustomFields
- Field field_:String { serializedName="field" }
- Field anotherField:String { serializedName = "for" }
- End Type
- ```
- 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
- the JSON object correctly.
- Here the example in full :
- ```blitzmax:
- SuperStrict
- Framework BRL.StandardIO
- Import Text.JConv
- Local data:String = "{~qfield~q:~qhello~q,~qfor~q:~qever~q}"
- Local jconv:TJConv = New TJConvBuilder.Build()
- Local custom:TCustomFields = TCustomFields(jconv.FromJson(data, "TCustomFields"))
- Print custom.field_
- Print custom.anotherField
- Type TCustomFields
- Field field_:String { serializedName="field" }
- Field anotherField:String { serializedName = "for" }
- End Type
- ```
- In addition to `serializedName`, another metadata property is available during deserialisation, `alternateName`. If you consider `serializedName` as being
- the default value, `alternateName` allows you to map other JSON keys to a particular field.
- For example, given a `TUser` object where we are already mapping the JSON key `full_name` to the field `name` :
- ```blitzmax
- Type TUser
- Field name:String { serializedName = "full_name" }
- Field email:String
- Field age:int
- End Type
- ```
- We decide we also want ingest similar data from another system in our application. Instead of `full_name`, the other system uses
- `username` for this value. Using the `alternateName` metadata property we can add a comma-delimited list of other names, and our #Type becomes :
- ```blitzmax
- Type TUser
- Field name:String { serializedName = "full_name", alternateName ="username" }
- Field email:String
- Field age:int
- End Type
- ```
- `alternateName` is only available during deserialisation. #Text.JConv will use either the #Field name or the `serializedName` when mapping a
- BlitzMax object to JSON.
- The following two sets of JSON would map to a `TUser` object and set the `name` #Field appropriately :
- ```json
- {
- "full_name" : "Bob",
- "email" : "[email protected]"
- }
- ```
- ```json
- {
- "username" : "userBob",
- "email" : "[email protected]"
- }
- ```
- 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,
- deserialising the JSON would result in the `name` #Field containing the value `userBob` :
- ```json
- {
- "full_name" : "Bob",
- "username" : "userBob",
- "email" : "[email protected]"
- }
- ```
- ## Ignoring Fields
- 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.
- The first, `transient`, completely disables field from mapping in either direction.
- If you want more finer grained control, the metadata properties `noSerialize` and `noDeserialize` can be used instead.
- The `noSerialize` property instructs #Text.JConv not to serialize a particular field to JSON, but it allows data from a JSON object to be
- deserialized into the #Field.
- On the other hand, `noDeserialize` prevents data from a JSON object from deserializing into the #Field, but does allow it to be serialized into
- a JSON object.
- We'll apply some properties to the `TUser` object to demonstrate the options :
- ```blitzmax
- Type TUser
- Field name:String
- Field email:String { noSerialize }
- Field age:int { noDeserialize }
- Field passwordHash:String { transient }
- End Type
- ```
- Based on the above example, when serializing an instance of `TUser`, only the `name` and `age` fields would be mapped to JSON.
- Similarly, only the `name` and `email` fields would be mapped from a JSON object.
- The following is a complete example of these properties in action :
- ```blitzmax
- SuperStrict
- Framework BRL.StandardIO
- Import Text.JConv
- Local user:TUser = New TUser("bob", "[email protected]", 30, "xxxx")
- Local jconv:TJConv = New TJConvBuilder.Build()
- Local json:String = jconv.ToJson(user)
- Print "json : " + json
- json = "{~qname~q: ~qbob~q, ~qemail~q: [email protected]~q, ~qage~q: 30, ~qpasswordHash~q: ~qxxxx~q}"
- user = TUser(jconv.FromJson(json, "TUser"))
- Print "name : " + user.name
- Print "email : " + user.email
- Print "age : " + user.age
- Print "hash : " + user.passwordHash
- Type TUser
- Field name:String
- Field email:String { noSerialize }
- Field age:Int { noDeserialize }
- Field passwordHash:String { transient }
- Method New(name:String, email:String, age:Int, ph:String)
- Self.name = name
- Self.email = email
- Self.age = age
- Self.passwordHash = ph
- End Method
- End Type
- ```
- ## Configuring TJConv with the Builder
- You may have noticed, that by default #Text.JConv serialises the JSON into a single line.
- You can change this behaviour with one ofthe builder's configurable options.
- 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.
- 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 :
- ```blitzmax
- Local jconv:TJConv = New TJConvBuilder.WithPrecision(2).WithCompact().Build()
- ```
- ### WithIndent
- The #WithIndent method of #TJConvBuilder specifies the number of spaces to use for indenting of nested objects. The default of 0 (zero)
- means not to use pretty-printing.
- This is an example of `TUser` using the default options :
- ```json
- {"name": "Bob", "email": "[email protected]", "age": 30}
- ```
- And this is an example of building with #WithIndent :
- ```json
- {
- "name": "Bob",
- "email": "[email protected]",
- "age": 30
- }
- ```
- ### WithCompact
- On the other hand, JSON can be compacted further using the #WithCompact method, which works to remove extra spaces :
- ```json
- {"name":"Bob","email":"[email protected]","age":30}
- ```
- ### WithPrecision
- The representation of decimal numbers can be controlled by the #WithPrecision method, which specifies the maximum number of decimal places to used.
- For example, the default representation of a #Type `TPoint` :
- ```blitzmax
- Type TPoint
- Field x:Double
- Field y:Double
- End Type
- ```
- would normally result in the following JSON with fields of the values (10.565, 15.912) :
- ```json
- {"x": 10.565, "y": 15.912000000000001}
- ```
- Using a maximum precision of 3 (`WithPrecision(3)`), the resulting JSON would become :
- ```json
- {"x": 10.565, "y": 15.912}
- ```
- ### WithEmptyArrays
- By default, #Null/empty arrays are not serialised at all. That is, the field is not included in the JSON object.
- The #WithEmptyArrays option can be enabled to generate an empty array (`[]`] instead.
- ### WithBoxing
- Primitive numbers, by their very nature in BlitzMax, have no concept of nullability. JSON, conversely, can represent any field as a null value,
- either by simply not including it in the object, or by having the value `null`.
- 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
- a value field of the appropriate numeric #Type. Using a boxed primitive then allows a field to contain a value, or be #Null.
- This feature is enabled by using the #WithBoxing option of the builder.
- 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
- greater than zero :
- ```json
- [
- {
- "jobId": "ABC123",
- "failures": 3,
- "lastError": "overflow"
- },
- {
- "jobId": "DEF456"
- }
- ]
- ```
- Deserialising this wouldn't be a problem, as our `TJob` #Type could represent no `failures` by the number zero :
- ```blitzmax
- Type TJob
- Field jobId:String
- Field failures:Int
- Field lastError:String
- End Type
- ```
- 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
- the `failures` #Field.
- Utilising the boxing feature, we could instead define the `failures` #Field as `TInt` :
- ```blitzmax
- Type TJob
- Field jobId:String
- Field failures:TInt
- Field lastError:String
- End Type
- ```
- Which would, for #Null values, result in the `features` #Field not being serialized to JSON.
- Here's a full example highlighting the use of boxing :
- ```blitzmax
- SuperStrict
- Framework BRL.StandardIO
- Import Text.JConv
- Local job1:TJob = New TJob("ABC123", 3, "overflow")
- Local job2:TBoxedJob = New TBoxedJob("DEF456", 0, Null)
- Local jconv:TJConv = New TJConvBuilder.WithBoxing().WithIndent(2).Build()
- Print jconv.ToJson(job1)
- Print jconv.ToJson(job2)
- Type TJob
- Field jobId:String
- Field failures:Int
- Field lastError:String
-
- Method New(jobId:String, failures:Int, lastError:String)
- Self.jobId = jobId
- Self.failures = failures
- Self.lastError = lastError
- End Method
- End Type
- Type TBoxedJob
- Field jobId:String
- Field failures:TInt
- Field lastError:String
- Method New(jobId:String, failures:Int, lastError:String)
- Self.jobId = jobId
- If failures > 0 Then
- Self.failures = New TInt(failures)
- End If
- Self.lastError = lastError
- End Method
- End Type
- ```
- Running the above example would result in the following output :
- ```
- {
- "jobId": "ABC123",
- "failures": 3,
- "lastError": "overflow"
- }
- {
- "jobId": "DEF456"
- }
- ```
- ### RegisterSerializer
|