{"id":20481899,"url":"https://github.com/typeable/schematic","last_synced_at":"2025-04-24T04:03:30.488Z","repository":{"id":55013085,"uuid":"92056519","full_name":"typeable/schematic","owner":"typeable","description":"type-safe JSON spec and validation tool","archived":false,"fork":false,"pushed_at":"2021-01-15T11:03:56.000Z","size":202,"stargazers_count":85,"open_issues_count":10,"forks_count":10,"subscribers_count":21,"default_branch":"master","last_synced_at":"2025-04-22T13:06:01.988Z","etag":null,"topics":["ghc","json-schema","jsonpath","schema-migrations"],"latest_commit_sha":null,"homepage":"","language":"Haskell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/typeable.png","metadata":{"files":{"readme":"README.md","changelog":"ChangeLog.md","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":"2017-05-22T13:26:10.000Z","updated_at":"2024-11-23T11:24:24.000Z","dependencies_parsed_at":"2022-08-14T09:01:08.421Z","dependency_job_id":null,"html_url":"https://github.com/typeable/schematic","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/typeable%2Fschematic","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/typeable%2Fschematic/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/typeable%2Fschematic/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/typeable%2Fschematic/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/typeable","download_url":"https://codeload.github.com/typeable/schematic/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250457787,"owners_count":21433727,"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":["ghc","json-schema","jsonpath","schema-migrations"],"created_at":"2024-11-15T16:10:13.626Z","updated_at":"2025-04-24T04:03:30.459Z","avatar_url":"https://github.com/typeable.png","language":"Haskell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# schematic\n\n[![Build Status](https://travis-ci.org/typeable/schematic.svg?branch=master)](https://travis-ci.org/typeable/schematic)\n\n## Goal\n\nThe goal of the library is to provide a type-safe transport layer for serializing and validating JSON. It can be thought of as a subset of [json-schema](http://json-schema.org), which is basically a specification of a JSON document. The other goal is getting as much as possible from this specification for free. Right now the following bits are prototyped:\n\n* All of the instantiations of the transport types structurally follow the schema provided by user\n* Serializers are generic, so they follow from the type-level schema and the're supposed to have a roundtrip property by an implementation.\n* Runtime value validators are generated from the schema. Validation errors are reported as a pairs of a json-path to the element and an error message.\n* There are migrations. It's possible to describe a series of migrations to the schema and have all the necessary machinery to deserialize a user specified version of the schema if there are multiple versions available.\n* Schematic schemas can be exported to [json-schema](http://json-schema.org)\n\nBe aware that library is experimental and subject to change a lot. The current state can be viewed as a prototype.\n\n## Installation\n\n* Install [Stack](https://github.com/commercialhaskell/stack)\n\n```\n$ stack install schematic\n```\n\n## GHC Extensions\n\nTo use this library without any hassle you should add a few GHC extension\neither to a module or a cabal file:\n\n```\nDataKinds\nOverloadedLists\nOverloadedStrings\nTypeApplications\n```\n\n`Overloaded`-extensions are being used only by `field` combinator,\nso if you don't use it - feel free to disable it.\n\n## GHC Options\n\nI recommend using `{-# OPTIONS_GHC -fno-warn-unticked-promoted-constructors #-}`\npragma in modules which declare schemas and schema migrations, because `schematic`\nheavily uses promoted types which should be prefixed with \"`\" signs and they can\nbe dropped with this option, making it less noisy visually.\nIt's entirely optional and totally opinionated, so feel free to ignore this.\nThrough the documentation, the code is written without any explicit ticks for\nthe same reason.\nNotice that they're still needed for type-level lists and tuples in schematic code.\n\n## Basic Examples\n\n```haskell\ntype SchemaExample\n  = SchemaObject\n    '[ '(\"foo\", SchemaArray '[AEq 1] (SchemaNumber '[NGt 10]))\n     , '(\"bar\", SchemaOptional (SchemaText '[TEnum '[\"foo\", \"bar\"]]))]\n```\n\n* This schema says that's structurally it's a JSON object which has two fields:\n  * field \"foo\" contains an array of numeric values\n  * field \"bar\" contains an optional text value\n* It also allows you to validate JSON values itself:\n  * an array in \"foo\" contains exactly one element\n  * that element must be strictly greater than 10\n  * value of \"bar\" field must be either \"foo\" or \"bar\" if presented\n\n\nThis one is valid for example\n\n```haskell\nschemaJson :: ByteString\nschemaJson = \"{\\\"foo\\\": [13], \\\"bar\\\": null}\"\n```\n\nAlso valid\n\n```haskell\nschemaJson :: ByteString\nschemaJson = \"{\\\"foo\\\": [13], \\\"bar\\\": \\\"bar\\\"}\"\n```\n\nit can be parsed and validated like this:\n\n```haskell\ndecodeAndValidateJson schemaJson :: ParseResult (JsonRepr SchemaExample)\n```\n\n`ParseResult` type encodes three possible situations:\n* json is structurally consistent with a schema and runtime validations pass\n* json source is malformed or doesn't correspond to a schema structurally\n* some of the runtime validations have failed, but it's structurally correct\n  overall\n\nIt implies a transport layer representation of that data that can be traversed\nand transformed to whatever internal types user has.\nIt's called `JsonRepr`. Type parameter is the schema itself.\n\n```haskell\njsonExample :: JsonRepr SchemaExample\njsonExample = withRepr @SchemaExample\n   $  field @\"foo\" [12]\n   :\u0026 field @\"bar\" (Just \"bar\")\n   :\u0026 RNil\n```\n\nThe most basic and sugar-free way of constructing the same object will look like this:\n\n```\njsonExample' :: JsonRepr SchemaExample\njsonExample' = ReprObject $\n  FieldRepr (ReprArray [ReprNumber 12])\n    :\u0026 FieldRepr (ReprOptional (Just (ReprText \"bar\")))\n    :\u0026 RNil\n```\n\n## Lens-compatibility\n\nIt's possible to use `flens` to construct a field lens for a typed object in schematic. Let's suppose you have a schema like this:\n\n```haskell\ntype ArraySchema = SchemaArray '[AEq 1] (SchemaNumber '[NGt 10])\n\ntype ArrayField = '(\"foo\", ArraySchema)\n\ntype FieldsSchema =\n  '[ ArrayField, '(\"bar\", SchemaOptional (SchemaText '[TEnum '[\"foo\", \"bar\"]]))]\n\ntype SchemaExample = 'SchemaObject FieldsSchema\n\n```\n\nThere are two ways of working with named fields in the objects:\n\n* Using the `fget` and `fset` functions: `fget @\"foo\" (fput newFooVal objectData) == newFooVal`\n* Using the lens library: `set (flens @\"foo\") newFooVal objectData ^. flens @\"foo\" == newFooVal`\n\n## Migrations\n\nUsually migrations are implicit and hard to deal with, `schematic` deals with it\nby giving tools to deal with it to a programmer, being as explicit as possible,\nnotifying the developer of a missing transition with a compile-time error.\nIt allows to build versioned HTTP APIs or migrate data from the older versions\nwhen reading from the JSON storages.\n\nIt's possible to represent schema changes as a series of migrations,\nwhich describes a series of json-path/change pairs. Migrations can be applied in\nsuccession.\n\nThis piece of code will apply a migration to the schema, decode and validate the latest version.\n\n```haskell\ntype SchemaExample\n  = SchemaObject\n    '[ '(\"foo\", SchemaArray '[AEq 1] (SchemaNumber '[NGt 10]))\n     , '(\"bar\", SchemaOptional (SchemaText '[TEnum '[\"foo\", \"bar\"]]))]\n\ntype TestMigration =\n  'Migration \"test_revision\"\n    '[ Diff '[ PKey \"bar\" ] (Update (SchemaText '[]))\n     , Diff '[ PKey \"foo\" ] (Update (SchemaNumber '[])) ]\n\ntype VS = 'Versioned SchemaExample '[ TestMigration ]\n\nschemaJsonTopVersion :: ByteString\nschemaJsonTopVersion = \"{ \\\"foo\\\": 42, \\\"bar\\\": \\\"bar\\\" }\"\n```\n\nIt's possible to decode the latest version like this:\n\n```\ndecodeAndValidateJson schemaJsonTopVersion :: ParseResult (JsonRepr (TopVersion (AllVersions VS)))\n```\n\nIt's important to differentiate between schema migrations and data migrations.\nSchema migration is a change of a schema acceptable by a JSON consumer. It transforms one schema to another.\nData migrations on the other hand are functions on the data itself. They transform values\nacceptable by one schema to values acceptable by another.\nLet's look at the situation when we have to add a field to a JSON Object of a current schema:\n\n```haskell\ntype SchemaExample = 'SchemaObject\n  '[ '(\"foo\", 'SchemaArray '[ 'AEq 1] ('SchemaNumber '[ 'NGt 10]))\n   , '(\"bar\", 'SchemaOptional ('SchemaText '[ 'TEnum '[\"foo\", \"bar\"]]))]\n\njsonExample :: JsonRepr SchemaExample\njsonExample = withRepr @SchemaExample\n   $ field @\"bar\" (Just \"bar\")\n  :\u0026 field @\"foo\" [12]\n  :\u0026 RNil\n\ntype AddQuuz =\n  'Migration \"add_field_quuz\"\n   '[ 'Diff '[] ('AddKey \"quuz\" (SchemaNumber '[])) ]\n\ntype DeleteQuuz =\n  'Migration \"remove_field_quuz\"\n    '[ 'Diff '[] ( 'DeleteKey \"quuz\") ]\n\ntype Migrations = '[ AddQuuz\n                   , DeleteQuuz ]\n\ntype VersionedJson = 'Versioned SchemaExample Migrations\n\nmigrationList :: MigrationList Identity VersionedJson\nmigrationList\n  =   (migrateObject (\\r -\u003e Identity $ field @\"quuz\" 42 :\u0026 r))\n  :\u0026\u0026 shrinkObject\n  :\u0026\u0026 MNil\n\n```\n\nIn this instance `Migrations` is a list of schema migrations and\n`MigrationList Identity VersionedJson` is a list of data migrations corresponding\nto the list of schema migrations: `migrateObject (\\r -\u003e Identity $ field @\"quuz\" 42 :\u0026 r))`\nwill be used at runtime to transform `JsonRepr SchemaExample` to\n`JsonRepr (SchemaByRevision \"add_quuz\" VersionedJson). `Identity` in the type of\n`MigrationList` is a monad we choose to run our migrations in. If there's need to\ndo database queries or something like that, it can be changed to something more\nappropriate by a user.\n`migrateObject` takes a function transforming old fields into new ones, `shrinkObject`\nallows to shrink the JSON object in case migration only removed fields.\n\n## How do I construct a value of `JsonRepr schema`?\n\n`JsonRepr schema` is a primary representation of a value serializable/deserializable\nto JSON, but with a twist. It's guaranteed to correspond a type-level schema,\nit's a type parameter of a same name.\nIt makes constructing it a little bit more involved, but luckily `schematic`\nprovides a DSL for doing so in a more straightforward fashion. An example would\nlook like this:\n\n```haskell\ntype SchemaExample = 'SchemaObject\n  '[ '(\"foo\", SchemaArray '[ AEq 1] (SchemaNumber '[NGt 10]))\n   , '(\"bar\", SchemaOptional (SchemaText '[TEnum '[\"foo\", \"bar\"]]))]\n\njsonExample = withRepr @SchemaExample\n   $  field @\"bar\" (Just \"bar\")\n   :\u0026 field @\"foo\" [12]\n   :\u0026 RNil\n```\n\n`@foo` syntax is an explicit type application, which is a feature of GHC 8+, it\nmakes type inference possible without relying on a bunch of `Proxy`s, which\nmakes it syntactically more terse.\n\n[GHC Type Applications](https://ghc.haskell.org/trac/ghc/wiki/TypeApplication)\n\n`schematic` provides instances for `OverloadedLists` and `OverloadedStrings`\nGHC extensions to make use of string and list literals for values in a previous\nexample.\n\n\n## Export to json-schema (draft 4)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftypeable%2Fschematic","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftypeable%2Fschematic","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftypeable%2Fschematic/lists"}