Builds models from JSON Schemas
Scala macros for generating code from Json Schemas. Any structures defined within the
schema (such as properties, enums, etc) are used to generate scala code at compile time. Json encoder/decoders are also generated for the Circe Json library.
NB:
Why Argus? In keeping with the theme of Argonaut and Circe, Argus (son of Arestor) was the builder of the ship “Argo”,
on which the Argonauts sailed.
Starting with this Json schema.
{
"title": "Example Schema",
"type": "object",
"definitions" : {
"Address": {
"type": "object",
"properties": {
"number" : { "type": "integer" },
"street" : { "type": "string" }
}
},
"ErdosNumber": {
"type": "integer"
}
},
"properties": {
"name": {
"type": "array",
"items": { "type": "string" }
},
"age": {
"description": "Age in years",
"type": "integer"
},
"address" : { "$ref" : "#/definitions/Address" },
"erdosNumber" : { "$ref" : "#/definitions/ErdosNumber" }
}
}
We can use the @fromSchemaResource macro to generate case classes for (Root, Address)
import argus.macros._
import io.circe._
import io.circe.syntax._
@fromSchemaResource("/simple.json", name="Person")
object Schema
import Schema._
import Schema.Implicits._
val json = """
|{
| "name" : ["Bob", "Smith"],
| "age" : 26,
| "address" : {
| "number" : 31,
| "street" : "Main St"
| },
| "erdosNumber": 123
|}
""".stripMargin
// Decode into generated case class
val person = parser.decode[Person](json).toOption.get
// Update address
val address = Address(number=Some(42), street=Some("Alt St"))
val newPerson = person.copy(address=Some(address))
// Encode base to json
newPerson.asJson
Many more examples here
Json | Generated Scala |
|
|
json type | json format | Generated Scala type |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
|
|
|
Json | Generated Scala |
|
|
Json | Generated Scala |
|
|
Json | Generated Scala |
|
|
Json | Generated Scala |
|
|
Json | Generated Scala |
|
|
And lastly: We only generate code from json schemas, but we can’t generate json-schema from code. This is fully possible, but
requires work ;)
There’s still a lot to do! Looking for contributors to address any of above.
All macros support arguments debug=true
and outPath="..."
. debug
causes the generated
code to be dumped to stdout, and outPath
causes the generated code to be written to a file.
The optional argument outPathPackage
allows to specify a package name for the output file.
@fromSchemaResource("/simple.json", debug=true, outPath="/tmp/Simple.Scala", outPathPackage="argus.simple")
object Test
You can generate code from inline json schemas. Also supported are fromSchemaInputStream
and fromSchemaURL
too.
@fromSchemaJson("""
{
"properties" : {
"name" : { "type" : "string" },
"age" : { "type" : "integer" }
},
"required" : ["name"]
}
""")
object Schema
You can name the root class that is generated via the name="..."
argument.
@fromSchemaResource("/simple.json", name="Person")
object Schema
import Schema.Person
Within the object we also generate json encoder/decoder implicit variables, but you need to import
them into scope.
@fromSchemaResource("/simple.json", name="Person")
object Schema
import Schema._
import Schema.Implicits._
Person(...).asJson
You can override specific Encoders/Decoders. All implicits are baked into a trait called LowPriorityImplicits.
Rather than importing Foo.Implicits you can make your own implicits object that extends this and provides overrides.
For example:
@fromSchemaResource("/simple.json")
object Foo
import Foo._
object BetterImplicits extends Foo.LowPriorityImplicits {
implicit val myEncoder: Encoder[Foo.Root] = ...
implicit val betterDecoder: Decoder[Foo.Root] = ...
}
import BetterImplicits._
Free form Json (we call them Any types above) are quite common within Json schemas. These are fields that are left open to take any
kind of Json chunk (maybe for additional properties, or data, etc). Unfortunately they presents a challenge in a strongly typed
language, such as Scala, where we always need some kind of type.
The approach we’ve taken is to wrap these chunks in their own case class which has a single field of type Any
.
This also allows you to override the encode/decoders for that type (Root.Data
in this example) with something more custom
if required.
@fromSchemaJson("""
{
"type": "object",
"properties" : {
"data" : { "type": "array", "items": { } }
}
}
""")
object Schema
import Schema._
import Schema.Implicits._
val values = List( Root.Data(Map("size" -> 350, "country" -> "US")), Root.Data(Map("size" -> 350, "country" -> "US")) )
Root(data=Some(values))
The default encoder/decoder (as shown in the code example above) works if your types are:
Maps[String, Any], where Any needs to be one of the types in this list.
Or, in other words, you can’t stick arbitrary objects in the Any wrapper and expect their encoders/decoders to get picked up.
If you need that then you’ll have to override the default encoder/decoder for this type.