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!
Install Json
with the Fantom Pod Manager ( FPM ):
C:\> fpm install afJson
Or install Json
with fanr:
C:\> fanr install -r http://eggbox.fantomfactory.org/fanr/ afJson
To use in a Fantom project, add a dependency to build.fan
:
depends = ["sys 1.0", ..., "afJson 2.0"]
Full API & fandocs are available on the Eggbox - the Fantom Pod Repository.
-
Create a text file called
Example.fan
using afJsonclass 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) // --> Emma echo(friend.car.name) // --> Golf friend.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.fanEmma Golf {"name":"Emma","sex":"female","score":11,"likes":["Cakes","Adventure"]}
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.
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.
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.
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)
}
}
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) // --> {}
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"}
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 everything
jsonConvs := 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 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})")
}
}