{"id":15494280,"url":"https://github.com/andyglow/relaxed-json-update","last_synced_at":"2025-07-28T12:43:19.708Z","repository":{"id":68021566,"uuid":"74860399","full_name":"andyglow/relaxed-json-update","owner":"andyglow","description":"Relaxed (partial) Scala case class update with json","archived":false,"fork":false,"pushed_at":"2016-12-02T22:55:24.000Z","size":43,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-01-12T23:28:48.718Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"lgpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andyglow.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":"2016-11-27T01:26:23.000Z","updated_at":"2016-11-27T01:32:31.000Z","dependencies_parsed_at":"2023-04-24T21:32:58.670Z","dependency_job_id":null,"html_url":"https://github.com/andyglow/relaxed-json-update","commit_stats":{"total_commits":16,"total_committers":1,"mean_commits":16.0,"dds":0.0,"last_synced_commit":"8d0c2d4387fc65acc24f1f5429e716970fae5699"},"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyglow%2Frelaxed-json-update","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyglow%2Frelaxed-json-update/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyglow%2Frelaxed-json-update/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyglow%2Frelaxed-json-update/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andyglow","download_url":"https://codeload.github.com/andyglow/relaxed-json-update/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241494321,"owners_count":19971905,"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":[],"created_at":"2024-10-02T08:13:04.885Z","updated_at":"2025-03-02T11:18:40.754Z","avatar_url":"https://github.com/andyglow.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Relaxed JSON Update\n[![Build Status](https://travis-ci.org/andyglow/relaxed-json-update.svg)](https://travis-ci.org/andyglow/relaxed-json-update)\n[![codecov](https://codecov.io/gh/andyglow/relaxed-json-update/branch/master/graph/badge.svg)](https://codecov.io/gh/andyglow/relaxed-json-update)\n\nRelaxed (partial) case class update with json.\n\n## Problem\nSometime, especially dealing with http/rest services, we want to have an ability to receive and update only certain\nfields of our resource/entity. Let me provide some short example (spray used).\n \nSuppose we are working on some Profile API\n```scala\ncase class Profile(id: String, name: String, password: String)\n```\nSo we have already created these scenarios\n* `GET    /profiles`      `get    \u0026 path(\"profiles\")`\n* `GET    /profiles/:id`  `get    \u0026 path(\"profiles\" / Segment)`\n* `POST   /profiles`      `post   \u0026 path(\"profiles\" / Segment) \u0026 entity(as[Profile])`\n* `DELETE /profiles/:id`  `delete \u0026 path(\"profiles\" / Segment)`\n\nAnd now we are about to implement\n* `PUT /profiles/:id`\n\n \u003e For simplicity of examples let's assume we use **sync** API here, but of course you should think twice,\n  how this feet your needs. Almost always you should use **async** approach.\n\n### Step 0\nThe very first idea that come to our mind is to reuse `Profile` instance like this:\n    \n```scala\n (put \u0026 path(\"profiles\" / Segment) \u0026 entity(as[Profile])) { (id, update) =\u003e\n   rejectEmptyResponse {\n     complete {\n       val entity: Option[Profile] = db get id\n       for {\n         entity \u003c- entity\n         updated = entity.copy(\n           name = update.name,\n           password = update.password)\n       } yield {\n         db.update(id, updated)\n         updated\n       } \n     }\n   }\n }\n```\n_Pros \u0026 Cons_\n\n* `id` field, which was defined as `String` has to be specified in payload, otherwise unmarshalling will fail.\n* We still can't update only certain fields.\n  \n### Step 1\nMake `id` optional\n \n```scala\ncase class Profile(id: Option[String], name: String, password: String)\n```\n\n_Pros \u0026 Cons_\n\nFrom one side it resolves issue with necessity to specify `id` twice in `uri` and in payload.\n\nFrom another\nside it brings us to an optional id hell as now we have to deal with it in our services handling it\nevery time by `getOrElse { throw new IllegalStateException }` or something.\n   \nAnd we still can't update partially.\n\n### Step 2\nMake Form case class where all fields are optional and `id` field is absent.\n \n```scala\ncase class Profile(id: String, name: String, password: String)\ncase class ProfileUpdate(name: Option[String], password: Option[String])\n```\n\n_Pros \u0026 Cons_\n\n* Now you we can omit `id` in payload.\n* You can update only necessary fields.\n \nBut\n* you have to have another one class. Just try to imagine how could it look like if you have a rich class with many fields.\n* you still have to handle all that fields manually. For example:\n```scala\ncase class ProfileUpdate(name: Option[String], password: Option[String]) {\n  def apply(profile: Profile): Profile = {\n    val _name = name getOrElse profile.name \n    val _password = password getOrElse profile.password\n     \n    profile.copy(\n      name = _name,\n      password = _password) \n  }\n}\n```\n```scala\n (put \u0026 path(\"profiles\" / Segment) \u0026 entity(as[ProfileUpdate])) { (id, update) =\u003e\n   rejectEmptyResponse {\n     complete {\n       val entity: Option[Profile] = db get id\n       for {\n         entity \u003c- entity\n         updated = update apply entity\n       } yield {\n         db.update(id, updated)\n         updated\n       } \n     }\n   }\n }\n```\nThis solution is much better but have one significant drawback. You have to write lot of boilerplate code.\nAgain. Just think about necessity to support this solution having rich class structure. It may become a nightmare.\n\n### Step 3\nWhat if we try to solve it without additional classes.\n \n```scala\n(put \u0026 path(\"profiles\" / Segment) \u0026 entity(as[JsValue])) { (id, json) =\u003e\n  rejectEmptyResponse {\n    complete {\n      val entity: Option[Profile] = db get id\n      for {entity \u003c- entity} yield {\n        for {\n          name \u003c- (json \\ \"name\").validateOpt[String]\n          password \u003c- (json \\ \"password\").validateOpt[String]\n        } yield {\n          val updated = entity.copy(\n            name = name,\n            password = password)\n           \n          db.update(id, updated)\n          updated\n        } toOption\n      } \n    }\n  }\n}\n```\n\n_Pros \u0026 Cons_\n\nEvent better as we can skip creating additional infrastructure (Form classes, Marshallers).\n\n## Idea\nSo what exactly this solution does is automate the approach we invented on step 3 by involving scala macros.\n\n1. You don't need to write special `*Update` classes.\n2. You don't need to write `copy` boilerplate.\n\nHow your code could look like by using this solution:\n```scala\n import com.github.andyglow.relaxed._\n import com.github.andyglow.relaxed.PlayJsonSupport._\n \n (put \u0026 path(\"profiles\" / Segment) \u0026 entity(as[JsValue])) { (id, update) =\u003e\n   rejectEmptyResponse {\n     complete {\n       val entity: Option[Profile] = db get id\n       for {\n         entity \u003c- entity\n         updated = Relaxed(entity) updated update\n       } yield {\n         db.update(id, updated)\n         updated\n       } \n     }\n   }\n }\n```\nThe same will work for `akka-http` as well.\n\nIt is also possible to mark certain fields as not participating in update.\n```scala\ncase class Profile(@skip id: String, name: String, password: String)\n```\n\n### SBT\n```scala\nlibraryDependencies += \"com.github.andyglow\" %% \"relaxed-json-update-api\" % \"${LATEST_VERSION}\"\n// and one of\nlibraryDependencies += \"com.github.andyglow\" %% \"relaxed-json-update-play-json\" % \"${LATEST_VERSION}\"\nlibraryDependencies += \"com.github.andyglow\" %% \"relaxed-json-update-spray-json\" % \"${LATEST_VERSION}\"\nlibraryDependencies += \"com.github.andyglow\" %% \"relaxed-json-update-jackson\" % \"${LATEST_VERSION}\"\nlibraryDependencies += \"com.github.andyglow\" %% \"relaxed-json-update-circe\" % \"${LATEST_VERSION}\"\nlibraryDependencies += \"com.github.andyglow\" %% \"relaxed-json-update-upickle\" % \"${LATEST_VERSION}\"\nlibraryDependencies += \"com.github.andyglow\" %% \"relaxed-json-update-argonaut\" % \"${LATEST_VERSION}\"\nlibraryDependencies += \"com.github.andyglow\" %% \"relaxed-json-update-json4s\" % \"${LATEST_VERSION}\"\n```\n\n- api\n  [ ![Download](https://api.bintray.com/packages/andyglow/scala-tools/relaxed-json-update-api/images/download.svg) ](https://bintray.com/andyglow/scala-tools/relaxed-json-update-api/_latestVersion)\n  [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-api_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-api_2.11)\n- play-json\n  [ ![Download](https://api.bintray.com/packages/andyglow/scala-tools/relaxed-json-update-play-json/images/download.svg) ](https://bintray.com/andyglow/scala-tools/relaxed-json-update-play-json/_latestVersion)\n  [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-play-json_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-play-json_2.11)\n- spray-json\n  [ ![Download](https://api.bintray.com/packages/andyglow/scala-tools/relaxed-json-update-spray-json/images/download.svg) ](https://bintray.com/andyglow/scala-tools/relaxed-json-update-spray-json/_latestVersion)\n  [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-spray-json_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-spray-json_2.11)\n- jackson (scala module)\n  [ ![Download](https://api.bintray.com/packages/andyglow/scala-tools/relaxed-json-update-jackson/images/download.svg) ](https://bintray.com/andyglow/scala-tools/relaxed-json-update-jackson/_latestVersion)\n  [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-jackson_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-jackson_2.11)\n- circe\n  [ ![Download](https://api.bintray.com/packages/andyglow/scala-tools/relaxed-json-update-circe/images/download.svg) ](https://bintray.com/andyglow/scala-tools/relaxed-json-update-circe/_latestVersion)\n  [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-circe_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-circe_2.11)\n- upickle\n  [ ![Download](https://api.bintray.com/packages/andyglow/scala-tools/relaxed-json-update-upickle/images/download.svg) ](https://bintray.com/andyglow/scala-tools/relaxed-json-update-upickle/_latestVersion)\n  [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-upickle_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-upickle_2.11)\n- argonaut\n  [ ![Download](https://api.bintray.com/packages/andyglow/scala-tools/relaxed-json-update-argonaut/images/download.svg) ](https://bintray.com/andyglow/scala-tools/relaxed-json-update-argonaut/_latestVersion)\n  [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-argonaut_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-argonaut_2.11)\n- json4s\n  [ ![Download](https://api.bintray.com/packages/andyglow/scala-tools/relaxed-json-update-json4s/images/download.svg) ](https://bintray.com/andyglow/scala-tools/relaxed-json-update-json4s/_latestVersion)\n  [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-json4s_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.andyglow/relaxed-json-update-json4s_2.11)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandyglow%2Frelaxed-json-update","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandyglow%2Frelaxed-json-update","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandyglow%2Frelaxed-json-update/lists"}