{"id":49683978,"url":"https://github.com/eignex/skema","last_synced_at":"2026-05-07T09:01:18.494Z","repository":{"id":356059173,"uuid":"1230850396","full_name":"Eignex/skema","owner":"Eignex","description":null,"archived":false,"fork":false,"pushed_at":"2026-05-06T12:47:41.000Z","size":90,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-06T13:39:55.351Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Eignex.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-06T11:36:13.000Z","updated_at":"2026-05-06T12:35:40.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Eignex/skema","commit_stats":null,"previous_names":["eignex/skema"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Eignex/skema","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Eignex%2Fskema","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Eignex%2Fskema/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Eignex%2Fskema/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Eignex%2Fskema/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Eignex","download_url":"https://codeload.github.com/Eignex/skema/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Eignex%2Fskema/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32730282,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-07T02:14:30.463Z","status":"ssl_error","status_checked_at":"2026-05-07T02:14:29.405Z","response_time":62,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":[],"created_at":"2026-05-07T09:01:17.094Z","updated_at":"2026-05-07T09:01:18.483Z","avatar_url":"https://github.com/Eignex.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://eignex.com/\"\u003e\n    \u003cpicture\u003e\n      \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/Eignex/.github/refs/heads/main/profile/banner-white.svg\"\u003e\n      \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/Eignex/.github/refs/heads/main/profile/banner.svg\"\u003e\n      \u003cimg alt=\"Eignex\" src=\"https://raw.githubusercontent.com/Eignex/.github/refs/heads/main/profile/banner.svg\" style=\"max-width: 100%; width: 22em;\"\u003e\n    \u003c/picture\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n# skema\n\n[![Maven Central](https://img.shields.io/maven-central/v/com.eignex/skema.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/com.eignex/skema)\n[![Build](https://github.com/eignex/skema/actions/workflows/build.yml/badge.svg)](https://github.com/eignex/skema/actions/workflows/build.yml)\n[![codecov](https://codecov.io/gh/eignex/skema/branch/main/graph/badge.svg)](https://codecov.io/gh/eignex/skema)\n[![License](https://img.shields.io/github/license/eignex/skema)](https://github.com/eignex/skema/blob/main/LICENSE)\n\nA Kotlin Multiplatform library for schemas that work two ways at once. Declare a schema as a singleton object on the producer side and you get typed compile-time access to every field; serialize the same definition to JSON, YAML, ProtoBuf, or any other kotlinx-serialization format, and a downstream consumer that doesn't share your Kotlin code can decode the schema and walk it by name. Both paths share the same SchemaDef wire shape, so producers and consumers mix and match without reinventing either side.\n\nIf you only need typed access, write a data class. If you only need wire data, write a sealed Serializable interface. skema is for the case where both are required.\n\nUsed by [kumulant](https://github.com/Eignex/kumulant) (streaming statistics), [klause](https://github.com/Eignex/klause) (constraint solver), and [combo](https://github.com/Eignex/combo) (multi-armed bandit), but not coupled to any of them; the library is generic plumbing.\n\n## A complete example\n\n```kotlin\nimplementation(\"com.eignex:skema:0.1.0\")\n```\n\nDefine a vocabulary of config types and a base schema with name-taking declarators. The convention is borrowed directly from JetBrains' [Exposed](https://github.com/JetBrains/Exposed):\n\n```kotlin\n@Serializable sealed interface FormField\n@Serializable @SerialName(\"Bool\") data object BoolField : FormField\n@Serializable @SerialName(\"Int\")  data class IntField(val min: Int, val max: Int) : FormField\n\nsealed interface FieldKey { val name: String }\ndata class BoolKey(override val name: String) : FieldKey\ndata class IntKey(override val name: String, val min: Int, val max: Int) : FieldKey\n\nabstract class FormSchema : Schema\u003cFormField\u003e() {\n    protected fun bool(name: String): BoolKey {\n        add(name, BoolField)\n        return BoolKey(name)\n    }\n    protected fun int(name: String, min: Int, max: Int): IntKey {\n        add(name, IntField(min, max))\n        return IntKey(name, min, max)\n    }\n\n    protected fun bool() = register(BoolField, ::BoolKey)\n    protected fun int(min: Int, max: Int) = register(IntField(min, max)) { IntKey(it, min, max) }\n}\n```\n\nA concrete schema is a singleton object. Mix-and-match assignment and delegation per property; both produce the same wire entry:\n\n```kotlin\nobject SignupFormSchema : FormSchema() {\n    val acceptsTos = bool(\"acceptsTos\")\n    val age       by int(13, 120)  // delegate: name from the property\n}\n\ndata class SignupResponse(val acceptsTos: Boolean, val age: Int)\n\nval response = SignupResponse(acceptsTos = true, age = 27)\nresponse.age  // typed Int\n\nval wire: String = SchemaJson.encodeToString(SchemaDef.serializer(FormField.serializer()), SignupFormSchema.definition())\n// {\"entries\":{\n//   \"acceptsTos\":{\"$type\":\"Bool\"},\n//   \"age\":{\"$type\":\"Int\",\"min\":13,\"max\":120}\n// }}\n```\n\nPick whichever form suits each entry: assignment makes the wire name explicit and survives property renames without changing the schema; the delegate form keeps the call site terse when the property name is the wire name. Schemas are plain singletons either way.\n\nA downstream consumer (different process, no SignupFormSchema class) decodes the same wire string and walks the entries by name:\n\n```kotlin\nval def = SchemaJson.decodeFromString(SchemaDef.serializer(FormField.serializer()), wire)\nfor ((name, config) in def.entries) when (config) {\n    is BoolField -\u003e renderCheckbox(name)\n    is IntField  -\u003e renderSlider(name, config.min, config.max)\n}\n```\n\nThe same schema serves both sides without the consumer needing the producer's Kotlin code. That's the win.\n\nThe default wire wraps entries under a field called `entries`. To ship under a different root name, or to add adjunct fields (klause carries `vars` plus `constraints`, for example), define a Serializable wrapper and expose it as a separate method:\n\n```kotlin\n@Serializable data class SignupForm(val fields: Map\u003cString, FormField\u003e)\n\nobject SignupFormSchema : FormSchema() {\n    val acceptsTos = bool(\"acceptsTos\")\n    val age        by int(13, 120)\n    fun signupDef() = SignupForm(entries)\n}\n\nSchemaJson.encodeToString(SignupForm.serializer(), SignupFormSchema.signupDef())\n// {\"fields\":{\"acceptsTos\":{\"$type\":\"Bool\"},\"age\":{\"$type\":\"Int\",\"min\":13,\"max\":120}}}\n```\n\n## Diff and composition\n\n`SchemaDef.diff(other)` reports per-entry adds, removes, and changes between two schemas. Use it to detect drift between producer and consumer versions, or to gate a schema migration:\n\n```kotlin\nval current = SignupFormSchema.definition()\nval incoming = SchemaJson.decodeFromString(SchemaDef.serializer(FormField.serializer()), wire)\nval diff = current.diff(incoming)\nif (!diff.isEmpty) error(\"Schema drifted: added=${diff.added.keys}, removed=${diff.removed.keys}, changed=${diff.changed.keys}\")\n```\n\nTwo schemas combine with `+`, with names prefixed via `namespaced(prefix)`. The combination throws on overlap, so plugin-style modules can compose without silent collisions:\n\n```kotlin\nval users   = userFieldsSchema.namespaced(\"user\")     // user.email, user.phone\nval billing = billingFieldsSchema.namespaced(\"billing\") // billing.address, ...\nval app     = users + billing\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feignex%2Fskema","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feignex%2Fskema","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feignex%2Fskema/lists"}