JsonUser Guide
Overview
Json is a customisable Fantom to Javascript Object Notation (JSON) mapping library.
It goes far beyond the usual JsonInStream
and JsonOutStream
classes by mapping and instantiating fully fledged Fantom domain objects.
Features:
- Converts all core Fantom types
- Converts nested / embedded objects
- Runs on Javascript platforms
- Simple to use
- JSON pretty printing
Just annotate fields with @JsonProperty
then call fromJson(...)
and toJson(...)
- couldn't be easier!
Quick Start
- Create a text file called
Example.fan
using afJson class Example { Void main() {
// write some JSON...json := """{ "name" : "Emma", "sex" : "female", "likes" : ["Cakes","Adventure"], "car" : { "name" : "Golf", "brand" : "VW" }, "score" : 9 }"""// ...and WHAM! A fully inflated domain object!friend := (Friend) Json().fromJson(json, Friend#) echo(friend.name)// --> Emmaecho(friend.car.name)// --> Golffriend.score = 11 friend.car = null// we can even convert the other way!moarJson := Json().toJson(friend) echo(moarJson)// --> {"name":"Emma","sex":"female","score":11,"likes":["Cakes","Adventure"]}} } class Friend { @JsonProperty Str name @JsonProperty Sex sex @JsonProperty Int score @JsonProperty Str[] likes @JsonProperty Car? car// embedded objects!new make(|This| f) { f(this) } } class Car { @JsonProperty Str name @JsonProperty Str brand new make(|This| f) { f(this) } } enum class Sex { male, female; } - Run
Example.fan
as a Fantom script from the command line:C:\> fan Example.fan Emma Golf {"name":"Emma","sex":"female","score":11,"likes":["Cakes","Adventure"]}
Terminology
JSON is the string representation of a Javascript object.
JsonObj is the Fantom representation of a JSON object. It only contains Maps
, Lists
, Bools
, Nums
, Strs
, and null
.
Entity is a Fantom object from your problem domain.
All conversion of Entities to and from JSON goes through an intermediary JsonObj
stage:
Entity <--> JsonObj <--> JSON
JsonReader
and JsonWriter
convert between JsonObjs and JSON.
JsonConverters
has methods to convert between all.
Usage
Any Fantom object may be converted to and from JSON. Just make sure that all fields to be converted are annotated with the @JsonProperty
facet.
Supported Types
The JSON Spec only defines types for Bool
, List
, Null
, Number
, Object
, and String
. As such, this library provides the following mappings:
 Fantom JSON ------------------ -------- afJson::JsLiteral <--> *as is* sys::Bool <--> Bool sys::Decimal <--> Number sys::Date <--> String sys::DateTime <--> String sys::Depend <--> String sys::Duration <--> String sys::Enum <--> String sys::Field <--> String sys::Float <--> Number sys::Int <--> Number sys::List <--> List sys::Locale <--> String sys::Map <--> Object sys::Method <--> String sys::MimeType <--> String sys::Num <--> Number null <--> Null sys::Obj <--> Object sys::Range <--> String sys::Regex <--> String sys::Slot <--> String sys::Str <--> String sys::Time <--> String sys::TimeZone <--> String sys::Type <--> String sys::Unit <--> String sys::Uri <--> String sys::Uuid <--> String sys::Version <--> String
Plus any Type
annotated with @Serializable { simple = true }
is converted to and from a Str
. Combined that accounts for all Fantom literals and core types.
Const vs Non-Const
This library can instantiate any Fantom object, both const
and non-const
. But if the type is const
, or if it has non-null
fields, then it must have an it-block ctor like the one below. That is the only way fields can be set during construction.
const class User { @JsonProperty const Str name// the it-block ctornew make(|This| f) { f(this) } }
Null vs Non-Null
Nullable fields are optional, that is, they do not require a JSON value.
class User { @JsonProperty Str? name// name is optional because it is nullablenew make(|This| f) { f(this) } } json := "{}" user := Json().fromJson(json, User#) as User echo(user.name)// --> null
Similarly, when converting an entity to JSON, null
values are not written out:
class User { @JsonProperty Str? name new make(|This| f) { f(this) } } user := User { name = null } json := Json().toJson(user) echo(json)// --> {}
Property Names
Sometimes you want the JSON name to be different to the field names. To facilitate this, set the @JsonProperty.name
attribute:
class User { @JsonProperty { name = "judge" } Str? name new make(|This| f) { f(this) } } user := User { name = "Dredd" } json := Json().toJson(user, User#) echo(json)// --> {"judge":"Dredd"}
Pickle Mode
Sometimes you wish to read / write objects to JSON that are outside of your control, meaning their fields won't be annotated with @JsonProperty
facets. To facilitate this, you can turn on Pickle Mode whereby all non @Transient
fields are converted, regardless of any @JsonProperty
facets. Data from @JsonProperty
facets, however, will still honoured if defined.
Pickle mode works by automatically writing out _type
properties, which are them used when re-inflating objects back.
Pickle Mode may be turned on globally as an option in JsonConverters
, or locally as an argument on the @JsonProperty
facets.
// turn on pickleMode for everythingjsonConvs := JsonConverters(null, [ "pickleMode" : true ])// ... or ...@Entity class User { @JsonProperty Str name @JsonProperty Int age** Turn on pickleMode just for this field** meta values may be *any* object@JsonProperty { pickleMode=true } Str:Obj? meta new make(|This|in) { in(this) } }
JSON and Dates
JSON does not define a Date object. As such, when writing Dates, they are serialised as ISO strings. At the other end, presumably in Javascript land, something must walk your object and de-serialise all your date strings back into Date objects.
But sometimes you want a quick hack and some people advocate inserting Javascript statements directly into the JSON. It may not be the best idea, but it's a good example of custom inspectors and converters...
using afJson class User { @JsonProperty Str? name @JsonProperty DateTime? timestamp } class Example { Void main() { jsonService := Json(JsonConverters.defConvs.setAll([ Date# : JsDateConverter() ])) user := User { name = "Judge Dredd"; timestamp = DateTime.now } json := jsonService.toJson(user) echo(json)// --> {"name":"Judge Dredd","timestamp":"new Date(1456178248297)"}} } const class JsDateConverter : JsonConverter { override Obj? fromJsonVal(Obj? jsonVal, JsonConverterCtx ctx) { throw UnsupportedErr() } override Obj? toJsonVal(Obj? fantomObj, JsonConverterCtx ctx) { fantomObj == null ? null : JsLiteral("new Date(${((DateTime) fantomObj).toJava})") } }