PATCH Requests đź”—
A PUT request normally replaces a whole resource, but a client often needs to update just one field. With ordinary optional fields, JSON cannot distinguish an omitted key (leave the existing value unchanged) from null (replace a nullable value with no value). Model whether the field was sent separately from its nullability so a PATCH endpoint can preserve that intent.
Define a Patch[T] codec whose default for a missing key is Keep; every present key becomes Set(value). Use Patch[String] for a required field and Patch[Option[String]] for a nullable field, so JSON null becomes Set(None). The runnable Scala CLI example below applies the patch to a user resource:
//> using scala 3.9.0
//> using dep ba.sake::sharaf-undertow:0.20.0
//> using dep ba.sake::tupson:0.31.0
import ba.sake.sharaf.*
import ba.sake.sharaf.undertow.UndertowSharafServer
import ba.sake.tupson.{given, *}
import org.typelevel.jawn.ast.*
enum Patch[+T]:
case Set(value: T)
case Keep
object Patch:
given [T](using valueRW: JsonRW[T]): JsonRW[Patch[T]] with
override def write(value: Patch[T]): JValue = value match
case Set(value) => valueRW.write(value)
case Keep => throw TupsonException("Patch.Keep can only be written as an object field")
override def shouldWriteField(value: Patch[T]): Boolean = value != Keep
override def parse(path: String, jValue: JValue): Patch[T] =
Set(valueRW.parse(path, jValue))
override def default: Option[Patch[T]] = Some(Keep)
case class User(name: String, address: Option[String]) derives JsonRW
case class UserPatch(name: Patch[String], address: Patch[Option[String]]) derives JsonRW
object Users:
private var user = User("Grace", Some("Arlington"))
def current: User = user
def patch(update: UserPatch): User =
user = User(
name = update.name match
case Patch.Set(value) => value
case Patch.Keep => user.name,
address = update.address match
case Patch.Set(value) => value
case Patch.Keep => user.address,
)
user
val routes = Routes:
case GET -> Path("user") => Response.withBody(Users.current)
case PATCH -> Path("user") =>
Response.withBody(Users.patch(Request.current.bodyJson[UserPatch]))
UndertowSharafServer("localhost", 8181, routes, exceptionMapper = ExceptionMapper.json).start()
println("Server started at http://localhost:8181")
Start it from examples/scala-cli:
scala patch_requests.sc
To update only the name, omit address:
curl -X PATCH http://localhost:8181/user \
-H 'Content-Type: application/json' \
-d '{"name":"Ada"}'
To clear the address, send it as null:
curl -X PATCH http://localhost:8181/user \
-H 'Content-Type: application/json' \
-d '{"address":null}'
For the JSON-specific behavior and a reusable standalone codec, see Tupson’s PATCH requests guide.