Handling PATCH requests đź”—

For an existing user with a required name and nullable address, a frontend expects these PATCH request bodies to mean:

            {}                    // keep every field unchanged
{ "address": null }  // clear the address
{ "name": "Ada" }   // replace the name

          

When a frontend sends a PATCH request without a field, it expects the existing value to remain unchanged. Model whether a field was sent separately from whether its value is nullable.

That distinction is awkward in many Scala JSON libraries. A field codec is often only asked to produce a JSON value, so it cannot say “omit my enclosing key” when the field should be left unchanged; doing so requires a custom product encoder.

Use a two-state Patch[T] with Tupson. Its shouldWriteField hook lets the codec omit a Keep field when serializing an object. Use Patch[String] for required fields and Patch[Option[String]] for nullable fields, so JSON null becomes Set(None).

Define the patch type and its codec:

            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 UserPatch(name: Patch[String], address: Patch[Option[String]]) derives JsonRW

          

Parse a request body. A missing key becomes Keep; a present value becomes Set, including JSON null as Set(None) for a nullable field:

            val patch = """{ "name": "Ada", "address": null }""".parseJson[UserPatch]
// UserPatch(Patch.Set("Ada"), Patch.Set(None))

          

When writing a UserPatch, Keep fields are omitted:

            UserPatch(Patch.Set("Ada"), Patch.Keep).toJson(spaces = 0)
// {"name":"Ada"}

          

Use Patch only as an object field. A top-level Patch.Keep has no JSON representation and fails to write.