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.fanusing 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.fanas 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 ctor
new 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 nullable
new 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})")
}
}