{"id":24803920,"url":"https://github.com/aishfenton/argus","last_synced_at":"2025-10-13T05:31:05.125Z","repository":{"id":57731569,"uuid":"62195388","full_name":"aishfenton/Argus","owner":"aishfenton","description":"Builds models from JSON Schemas","archived":false,"fork":false,"pushed_at":"2020-07-29T12:35:56.000Z","size":156,"stargazers_count":103,"open_issues_count":13,"forks_count":19,"subscribers_count":13,"default_branch":"master","last_synced_at":"2025-01-30T06:13:48.998Z","etag":null,"topics":["json-encoder","json-schema","scala"],"latest_commit_sha":null,"homepage":null,"language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aishfenton.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-06-29T04:25:37.000Z","updated_at":"2024-12-26T11:25:55.000Z","dependencies_parsed_at":"2022-09-07T19:41:16.418Z","dependency_job_id":null,"html_url":"https://github.com/aishfenton/Argus","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aishfenton%2FArgus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aishfenton%2FArgus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aishfenton%2FArgus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aishfenton%2FArgus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aishfenton","download_url":"https://codeload.github.com/aishfenton/Argus/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":236304098,"owners_count":19127438,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["json-encoder","json-schema","scala"],"created_at":"2025-01-30T06:13:52.725Z","updated_at":"2025-10-13T05:30:59.811Z","avatar_url":"https://github.com/aishfenton.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Argus (船大工)\n\n[![TravisCI](https://travis-ci.org/aishfenton/Argus.svg?branch=master)](https://travis-ci.org/aishfenton/Argus)\n\n\u003cimg src=\"https://www.vegas-viz.org/images/argus-logo.png\" width=\"170\"\u003e\n\nScala macros for generating code from [Json Schemas](http://json-schema.org). Any structures defined within the\nschema (such as properties, enums, etc) are used to generate scala code at compile time. Json encoder/decoders are also generated for the [Circe](https://github.com/travisbrown/circe) Json library.\n\n**NB:**\nWhy Argus? In keeping with the theme of Argonaut and Circe, Argus (son of Arestor) was the builder of the ship \"Argo\", \non which the Argonauts sailed.\n\n## Quick Example\n\nStarting with this Json schema.\n```json\n{\n  \"title\": \"Example Schema\",\n  \"type\": \"object\",\n  \"definitions\" : {\n    \"Address\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"number\" : { \"type\": \"integer\" },\n        \"street\" : { \"type\": \"string\" }\n      }\n    },\n    \"ErdosNumber\": {\n      \"type\": \"integer\"\n    }\n  },\n  \"properties\": {\n    \"name\": {\n      \"type\": \"array\",\n      \"items\": { \"type\": \"string\" }\n    },\n    \"age\": {\n      \"description\": \"Age in years\",\n      \"type\": \"integer\"\n    },\n    \"address\" : { \"$ref\" : \"#/definitions/Address\" },\n    \"erdosNumber\" : { \"$ref\" : \"#/definitions/ErdosNumber\" }\n  }\n}\n```\n\nWe can use the @fromSchemaResource macro to generate case classes for (Root, Address)\n\n```scala\nimport argus.macros._\nimport io.circe._\nimport io.circe.syntax._\n\n@fromSchemaResource(\"/simple.json\", name=\"Person\")\nobject Schema\nimport Schema._\nimport Schema.Implicits._\n\nval json = \"\"\"\n   |{\n   |  \"name\" : [\"Bob\", \"Smith\"],\n   |  \"age\" : 26,\n   |  \"address\" : {\n   |    \"number\" : 31,\n   |    \"street\" : \"Main St\"\n   |  },\n   |  \"erdosNumber\": 123\n   |}\n  \"\"\".stripMargin\n\n// Decode into generated case class\nval person = parser.decode[Person](json).toOption.get\n\n// Update address \nval address = Address(number=Some(42), street=Some(\"Alt St\"))\nval newPerson = person.copy(address=Some(address))\n\n// Encode base to json\nnewPerson.asJson \n```\n\n**Many more examples [here](argus/src/test/scala/argus/macros/FromSchemaSpec.scala)**\n\n# Rules\n\n## Supported constructs\n\n### Object templates (i.e. classes)\n\u003ctable\u003e\n\u003ctr\u003e\u003ctd\u003eJson\u003c/td\u003e\u003ctd\u003eGenerated Scala\u003c/td\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cpre\u003e\n{\n  \"properties\" : { \n    \"name\" : { \"type\" : \"string\" },\n    \"age\"  : { \"type\" : \"integer\" }\n  },\n  \"required\" : [\"name\"]\n}\n\u003c/pre\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cpre\u003e\ncase class Root(name: String, age: Option[Int] = None)\n\u003c/pre\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n### Basic types\n\u003ctable\u003e\n\u003ctr\u003e\u003ctd\u003ejson type\u003c/td\u003e\u003ctd\u003ejson format\u003c/td\u003e\u003ctd\u003eGenerated Scala type\u003c/td\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cpre\u003estring\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003e*\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003eString\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cpre\u003estring\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003euuid\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003ejava.util.UUID\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cpre\u003estring\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003edate-time\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003ejava.time.ZonedDateTime\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cpre\u003einteger\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003e* | int32\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003eInt\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cpre\u003einteger\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003eint64\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003eLong\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cpre\u003einteger\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003eint16\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003eShort\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cpre\u003einteger\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003eint8\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003eByte\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cpre\u003enumber\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003e* | double\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003eDouble\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cpre\u003enumber\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003esingle\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003eFloat\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cpre\u003eboolean\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003e*\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003eBoolean\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003ctr\u003e\u003ctd\u003e\u003cpre\u003enull\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003e*\u003c/pre\u003e\u003c/td\u003e\u003ctd\u003e\u003cpre\u003eNull\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\n\u003c/table\u003e\n\n### Definitions (i.e. common class definitions)\n\u003ctable\u003e\n\u003ctr\u003e\u003ctd\u003eJson\u003c/td\u003e\u003ctd\u003eGenerated Scala\u003c/td\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cpre\u003e\n{\n  \"definitions\" : { \n    \"Person\" : { ... },\n    ...\n  }\n  \"properties\" : { \n    \"person\" : { \"$ref\" : \"#/definitions/Person\" }\n  }\n}\n\u003c/pre\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cpre\u003e\ncase class Person(...)\ncase class Root(person: Option[Person] = None)\n\u003c/pre\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n### OneOf (i.e. type A or B) \n\u003ctable\u003e\n\u003ctr\u003e\u003ctd\u003eJson\u003c/td\u003e\u003ctd\u003eGenerated Scala\u003c/td\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cpre\u003e\n{\n  \"oneOf\": [\n    { \"$ref\" : \"#/definitions/Address\" },\n    { \"type\" : \"number\" }\n  ]\n}\n\u003c/div\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cpre\u003e\n@union sealed trait RootUnion\ncase class RootAddress(...) extends RootUnion\ncase class RootDouble(...) extends RootUnion\n\u003c/pre\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n### Enums\n\n\u003ctable\u003e\n\u003ctr\u003e\u003ctd\u003eJson\u003c/td\u003e\u003ctd\u003eGenerated Scala\u003c/td\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cpre\u003e\n{\n  \"properties\": { \n    \"countries\" : { \"enum\" : [\"NZ\", \"US\", \"UK\"] }\n  }\n}\n\u003c/div\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cpre\u003e\n@enum sealed trait Countries\nobject CountriesEnum {\n  case object NZ(...) extends Countries\n  case object US(...) extends Countries\n  ...\n}\ncase class Root(countries: Option[Countries] = None)\n\u003c/pre\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n### Arrays \n\n\u003ctable\u003e\n\u003ctr\u003e\u003ctd\u003eJson\u003c/td\u003e\u003ctd\u003eGenerated Scala\u003c/td\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cpre\u003e\n{\n  \"properties\": { \n    \"places\" : { \"items\" : { \"type\": \"string\" } }\n  }\n}\n\u003c/div\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cpre\u003e\ncase class Root(places: Option[List[String]] = None)\n\u003c/pre\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n### Any Types (i.e. when a field can take arbitrary values)\n\n\u003ctable\u003e\n\u003ctr\u003e\u003ctd\u003eJson\u003c/td\u003e\u003ctd\u003eGenerated Scala\u003c/td\u003e\n\u003ctr\u003e\n\u003ctd\u003e\u003cpre\u003e\n{\n  \"properties\": { \n    \"values\" : { }\n  }\n}\n\u003c/div\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd\u003e\u003cpre\u003e\ncase class Values(x: Any)\ncase class Root(values: Option[Values] = None)\n\u003c/pre\u003e\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n## Unsupported\n\n* Only Circe encoders/decoders are supported, although the skeleton is laid out for adding support for other Json libraries.\n* *anyOf* / *allOf*. Should be simple to add, just haven't done it yet.\n* *default*. Schemas can specify the default value to use for a field. Currently we just ignore these.\n* *not*. Not sure how you could specify this in a type language. Needs more thoughts\n* *additionalProperties*, and *additionalItems*. Json schema lets you specify free-form properties too. These are unsupported \nfor now (although I think this should be easy now there's supprot for Any types)\n* *patternProperties*. What should be the objects fields in this case? Maybe we just make it a Map?\n* *dependencies*. Dependencies can also extend the schema... sigh.\n* Any of the validation-only info, such as maxItems, since it doesn't contribute to the structure.\n\nAnd lastly: We only generate code from json schemas, but we can't generate json-schema from code. This is fully possible, but \nrequires work ;)\n\nThere's still a lot to do! Looking for contributors to address any of above.\n\n# Usage Tips\n\n1. All macros support arguments ```debug=true``` and ```outPath=\"...\"```. ```debug``` causes the generated \ncode to be dumped to stdout, and ```outPath``` causes the generated code to be written to a file. \nThe optional argument ```outPathPackage``` allows to specify a package name for the output file.\n \n    ```scala\n    @fromSchemaResource(\"/simple.json\", debug=true, outPath=\"/tmp/Simple.Scala\", outPathPackage=\"argus.simple\")\n    object Test\n    ```\n\n3. You can generate code from inline json schemas. Also supported are ```fromSchemaInputStream```\nand ```fromSchemaURL``` too.\n\n    ```scala\n    @fromSchemaJson(\"\"\"\n    {\n      \"properties\" : { \n        \"name\" : { \"type\" : \"string\" },\n        \"age\"  : { \"type\" : \"integer\" }\n      },\n      \"required\" : [\"name\"]\n    }\n    \"\"\")\n    object Schema\n    ```\n\n4. You can name the root class that is generated via the ```name=\"...\"``` argument.\n \n    ```scala\n    @fromSchemaResource(\"/simple.json\", name=\"Person\")\n    object Schema\n    import Schema.Person\n    ```\n\n5. Within the object we also generate json encoder/decoder implicit variables, but you need to import \nthem into scope. \n\n    ```scala\n    @fromSchemaResource(\"/simple.json\", name=\"Person\")\n    object Schema\n    import Schema._\n    import Schema.Implicits._\n    \n    Person(...).asJson\n    ```\n6. You can override specific Encoders/Decoders. All implicits are baked into a trait called LowPriorityImplicits.\nRather than importing Foo.Implicits you can make your own implicits object that extends this and provides overrides.\nFor example:\n\n    ```scala\n    @fromSchemaResource(\"/simple.json\")\n    object Foo\n    import Foo._\n\n    object BetterImplicits extends Foo.LowPriorityImplicits {\n      implicit val myEncoder: Encoder[Foo.Root] =   ... \n      implicit val betterDecoder: Decoder[Foo.Root] = ...\n    }\n    import BetterImplicits._\n    ```\n\n7. 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\nkind of Json chunk (maybe for additional properties, or data, etc). Unfortunately they presents a challenge in a strongly typed \nlanguage, such as Scala, where we always need some kind of type. \n \n    The approach we've taken is to wrap these chunks in their own case class which has a single field of type ```Any```. \n    This also allows you to override the encode/decoders for that type (```Root.Data``` in this example) with something more custom\n    if required.\n    \n    ```scala\n    @fromSchemaJson(\"\"\"\n    {\n      \"type\": \"object\",\n      \"properties\" : { \n        \"data\" : { \"type\": \"array\", \"items\": { } }\n      }\n    }\n    \"\"\")\n    object Schema\n    import Schema._\n    import Schema.Implicits._\n    \n    val values = List( Root.Data(Map(\"size\" -\u003e 350, \"country\" -\u003e \"US\")), Root.Data(Map(\"size\" -\u003e 350, \"country\" -\u003e \"US\")) )\n    Root(data=Some(values))\n    ```\n    \n    The default encoder/decoder (as shown in the code example above) works if your types are:\n    \n      * Primitive types: Boolean, Byte, Short, Int, Long, Float, Double\n      * Primate arrays (Array[Byte], Array[Int], etc)\n      * Seq\\[Any\\] (and subclasses) where Any needs to be one of the types in this list\n      * Maps[String, Any], where Any needs to be one of the types in this list.\n      \n    Or, in other words, you can't stick arbitrary objects in the Any wrapper and expect their encoders/decoders to get picked up. \n    If you need that then you'll have to override the default encoder/decoder for this type. \n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faishfenton%2Fargus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faishfenton%2Fargus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faishfenton%2Fargus/lists"}