{"id":16196038,"url":"https://github.com/alexarchambault/data-class","last_synced_at":"2025-04-13T07:47:00.468Z","repository":{"id":40346801,"uuid":"211907140","full_name":"alexarchambault/data-class","owner":"alexarchambault","description":"case-classes with better binary compatibility story","archived":false,"fork":false,"pushed_at":"2024-08-21T12:55:41.000Z","size":134,"stargazers_count":79,"open_issues_count":13,"forks_count":6,"subscribers_count":8,"default_branch":"main","last_synced_at":"2024-10-11T08:46:24.616Z","etag":null,"topics":["case-class","scala"],"latest_commit_sha":null,"homepage":"","language":"Scala","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/alexarchambault.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}},"created_at":"2019-09-30T16:41:50.000Z","updated_at":"2024-02-05T08:45:36.000Z","dependencies_parsed_at":"2024-10-25T17:10:40.785Z","dependency_job_id":"44bfd54e-28f5-41ca-af91-11f0547a2eb9","html_url":"https://github.com/alexarchambault/data-class","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexarchambault%2Fdata-class","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexarchambault%2Fdata-class/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexarchambault%2Fdata-class/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexarchambault%2Fdata-class/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alexarchambault","download_url":"https://codeload.github.com/alexarchambault/data-class/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247123094,"owners_count":20887260,"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":["case-class","scala"],"created_at":"2024-10-10T08:46:25.196Z","updated_at":"2025-04-04T05:06:20.932Z","avatar_url":"https://github.com/alexarchambault.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# data-class\n\n[![Build Status](https://travis-ci.org/alexarchambault/data-class.svg?branch=master)](https://travis-ci.org/alexarchambault/data-class)\n[![Maven Central](https://img.shields.io/maven-central/v/io.github.alexarchambault/data-class_2.13.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.alexarchambault/data-class_2.13)\n\n*data-class* allows to create classes almost like case-classes, but with no\npublic `unapply` or `copy` methods, making it easier to add fields to them while\nmaintaining binary compatiblity.\n\n## Usage\n\n### Setup\n\nAdd to your `build.sbt`,\n```scala\nlibraryDependencies += \"io.github.alexarchambault\" %% \"data-class\" % \"0.2.6\"\n```\n\nThe latest version is [![Maven Central](https://img.shields.io/maven-central/v/io.github.alexarchambault/data-class_2.13.svg)](https://maven-badges.herokuapp.com/maven-central/io.github.alexarchambault/data-class_2.13).\n\nThe macro paradise plugin is needed up to scala 2.12, and the right\ncompiler option needs to be used from 2.13 onwards:\n```scala\nlazy val isAtLeastScala213 = Def.setting {\n  import Ordering.Implicits._\n  CrossVersion.partialVersion(scalaVersion.value).exists(_ \u003e= (2, 13))\n}\nlibraryDependencies ++= {\n  if (isAtLeastScala213.value) Nil\n  else Seq(compilerPlugin(\"org.scalamacros\" % \"paradise\" % \"2.1.1\" cross CrossVersion.full))\n}\nscalacOptions ++= {\n  if (isAtLeastScala213.value) Seq(\"-Ymacro-annotations\")\n  else Nil\n}\n```\n\nLastly, if you know what you are doing, you can manage to have data-class\n[be a compile-time only dependency](https://stackoverflow.com/questions/21515325/add-a-compile-time-only-dependency-in-sbt/21516954#21516954).\n\n### API\n\nUse a `@data` annotation instead of a `case` modifier, like\n```scala\nimport dataclass.data\n\n@data class Foo(n: Int, s: String)\n```\n\nThis annotation adds a number of features, that can also be found in\ncase classes:\n- sensible `equals` / `hashCode` / `toString` implementations,\n- `apply` methods in the companion object for easier creation,\n- extend the `scala.Product` trait (itself extending `scala.Equal`), and\nimplement its methods,\n- extend the `scala.Serializable` trait.\n\nIt also adds things that differ from case classes:\n- add `final` modifier to the class,\n- for each field, add a corresponding `with` method (field `count: Int`\ngenerates a method `withCount(count: Int)` returning a new instance of the\nclass with `count` updated).\n\nMost notably, it does _not_ generate `copy` or `unapply` methods, making\nbinary compatibility much more tractable upon adding new fields (see below).\n\nIn the example above, the `@data` macro generates code like the following (modulo macro hygiene):\n```scala\nfinal class Foo(val n: Int, val s: String) extends Product with Serializable {\n\n  def withN(n: Int) = new Foo(n = n, s = s)\n  def withS(s: String) = new Foo(n = n, s = s)\n\n  override def toString: String = {\n    val b = new StringBuilder(\"Foo(\")\n    b.append(String.valueOf(n))\n    b.append(\", \")\n    b.append(String.valueOf(s))\n    b.append(\")\")\n    b.toString\n  }\n\n  override def canEqual(obj: Any): Boolean = obj != null \u0026\u0026 obj.isInstanceOf[Foo]\n  override def equals(obj: Any): Boolean = this.eq(obj.asInstanceOf[AnyRef]) || canEqual(obj) \u0026\u0026 {\n    val other = obj.asInstanceOf[Foo]\n    n == other.n \u0026\u0026 s == other.s\n  })\n\n  override def hashCode: Int = {\n    var code = 17 + \"Foo\".##\n    code = 37 * code + n.##\n    code = 37 * code + s.##\n    37 * code\n  }\n\n  private def tuple = (this.n, this.s)\n\n  override def productArity: Int = 2\n  override def productElement(n: Int): Any = n match {\n    case 0 =\u003e this.n\n    case 1 =\u003e this.s\n    case n =\u003e throw new IndexOutOfBoundsException(n.toString)\n  }\n}\n\nobject Foo {\n  def apply(n: Int, s: String): Foo = new Foo(n, s)\n}\n```\n\n### shapeless\n\nBy default, the classes annotated with `@data` now have a shape that\n`shapeless.Generic` handles:\n```scala\nimport dataclass.data\n\n@data class Foo(n: Int, d: Double)\n\nimport shapeless._\nGeneric[Foo] // works\n```\n\nNote that with shapeless `2.3.3` and prior versions, `Generic` derivation may fail\nif the body of the `@data` class contains `val`s or `lazy val`s, see\n[shapeless issue #934](https://github.com/milessabin/shapeless/issues/934).\n\n### Adding fields\n\nIn order to retain binary compatibility when adding fields, one should:\n- annotate the first added field with `dataclass.since`,\n- provide default values for the added fields, like\n```scala\nimport dataclass._\n\n@data class Foo(n: Int, d: Double, @since s: String = \"\", b: Boolean = false)\n```\n\nThe `@since` annotation makes the `@data` macro generate `apply` methods\ncompatible with those without the new fields.\n\nThe example above generates the following `apply` methods in the companion object of `Foo`:\n```scala\nobject Foo {\n  def apply(n: Int, d: Double): Foo = new Foo(n, d, \"\", false)\n  def apply(n: Int, d: Double, s: String, b: Boolean) = new Foo(n, d, s, b)\n}\n```\n\nThe `@since` annotation accepts an optional string argument - a version\ncan be passed for example - and it can be used multiple times, like\n```scala\nimport dataclass._\n\n@data class Foo(\n  n: Int,\n  d: Double,\n  @since(\"1.1\")\n  s: String = \"\",\n  b: Boolean = false,\n  @since(\"1.2\")\n  count: Option[Int] = None,\n  info: Option[String] = None\n)\n```\n\nThis generates the following `apply` methods in the companion object of `Foo`:\n```scala\nobject Foo {\n  def apply(n: Int, d: Double): Foo = new Foo(n, d, \"\", false, None, None)\n  def apply(n: Int, d: Double, s: String, b: Boolean) = new Foo(n, d, s, b, None, None)\n  def apply(n: Int, d: Double, s: String, b: Boolean, count: Option[Int], info: Option[String]) = new Foo(n, d, s, b, count, info)\n}\n```\n\n## Related work\n\n- [contraband](https://github.com/sbt/contraband) relies on code generation from\nJSON or a custom schema language to generate classes that can be evolved in a\nbinary compatible way\n- [stalagmite](https://gitlab.com/fommil/attic/tree/master/stalagmite) generates\ncase classes with custom features via some macros (but doesn't aim at helping\nmaintaining binary compatibility)\n- [`@unroll` support](https://github.com/scala/scala3/pull/21693) in Scala 3\nshould help evolving methods and classes a binary-compatible way\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexarchambault%2Fdata-class","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexarchambault%2Fdata-class","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexarchambault%2Fdata-class/lists"}