Skip to content

Commit 9a7add9

Browse files
Add example to unambiguous patching from JSON
1 parent 659776a commit 9a7add9

File tree

1 file changed

+105
-0
lines changed

1 file changed

+105
-0
lines changed

docs/docs/cookbook.md

+105
Original file line numberDiff line numberDiff line change
@@ -2948,6 +2948,111 @@ fields from the patching:
29482948
// Foo(a = "a", b = "d")
29492949
```
29502950

2951+
## Patching optional field with value decoded from JSON
2952+
2953+
JSON cannot define a nested optional values - since there is no wrapper like `Some` there is no way to represent difference between
2954+
`Some(None)` and `None` using build-in JSON semantics. If during `POST` request one want to always use `Some` values to **update**,
2955+
and `None` values to always indicate *keep old* semantics **or** always indicate *clear value* semantics (if the modified value is `Option` as well),
2956+
this is enough.
2957+
2958+
The problem, arises when one wantes to express 3 possible outcomes for modifying an `Option` value: *update value*/*keep old*/*clear value*.
2959+
2960+
The only solution in such case is to express in the API the 3 possible outcomes somwhow without resorting to nested `Option`s. As long as it can
2961+
be done, the type can be converted to nested `Option`s which have unambguous semantics:
2962+
2963+
!!! example
2964+
2965+
```scala
2966+
//> using dep io.scalaland::chimney::{{ chimney_version() }}
2967+
//> using dep com.lihaoyi::pprint::{{ libraries.pprint }}
2968+
//> using dep io.circe::circe-generic-extras::0.14.4
2969+
//> using dep io.circe::circe-parser::0.14.10
2970+
import io.circe.{Encoder, Decoder}
2971+
import io.circe.generic.extras.Configuration
2972+
import io.circe.generic.extras.auto._
2973+
import io.circe.generic.extras.semiauto._
2974+
import io.circe.parser.decode
2975+
import io.circe.syntax._
2976+
2977+
// An example of representing set-clean-keep operations in a way that cooperates with JSONs.
2978+
sealed trait OptionalUpdate[+A] extends Product with Serializable {
2979+
2980+
def toOption: Option[Option[A]] = this match {
2981+
case OptionalUpdate.Set(value) => Some(Some(value))
2982+
case OptionalUpdate.Clear => Some(None)
2983+
case OptionalUpdate.Keep => None
2984+
}
2985+
}
2986+
object OptionalUpdate {
2987+
2988+
case class Set[A](value: A) extends OptionalUpdate[A]
2989+
case object Clear extends OptionalUpdate[Nothing]
2990+
case object Keep extends OptionalUpdate[Nothing]
2991+
2992+
private implicit val customConfig: Configuration =
2993+
Configuration.default
2994+
.withDiscriminator("action")
2995+
.withSnakeCaseConstructorNames
2996+
2997+
implicit def encoder[A: Encoder]: Encoder[OptionalUpdate[A]] =
2998+
deriveConfiguredEncoder
2999+
implicit def decoder[A: Decoder]: Decoder[OptionalUpdate[A]] =
3000+
deriveConfiguredDecoder
3001+
}
3002+
3003+
case class Foo(field: Option[String], anotherField: String)
3004+
3005+
case class FooUpdate(field: OptionalUpdate[String])
3006+
object FooUpdate {
3007+
3008+
private implicit val customConfig: Configuration = Configuration.default
3009+
implicit val encoder: Encoder[FooUpdate] = deriveConfiguredEncoder
3010+
implicit val decoder: Decoder[FooUpdate] = deriveConfiguredDecoder
3011+
}
3012+
3013+
import io.scalaland.chimney.Patcher
3014+
import io.scalaland.chimney.dsl._
3015+
3016+
// This utility allows to automatically handle Option patching with OptionalUpdate values.
3017+
implicit def patchWithOptionalUpdate[A, Patch](implicit
3018+
inner: Patcher.AutoDerived[Option[A], Option[Option[A]]]
3019+
): Patcher[Option[A], OptionalUpdate[A]] = (obj, patch) =>
3020+
obj.patchUsing(patch.toOption)
3021+
3022+
pprint.pprintln(
3023+
decode[FooUpdate](
3024+
"""{ "field": { "action": "set", "value": "new-value" } }"""
3025+
) match {
3026+
case Left(error) => println(error)
3027+
case Right(patch) => Foo(Some("old-value"), "another-value").patchUsing(patch)
3028+
}
3029+
)
3030+
// expected output:
3031+
// Foo(field = Some(value = "new-value"), anotherField = "another-value")
3032+
pprint.pprintln(
3033+
decode[FooUpdate](
3034+
"""{ "field": { "action": "clear" } }"""
3035+
) match {
3036+
case Left(error) => println(error)
3037+
case Right(patch) => Foo(Some("old-value"), "another-value").patchUsing(patch)
3038+
}
3039+
)
3040+
// expected output:
3041+
// Foo(field = None, anotherField = "another-value")
3042+
pprint.pprintln(
3043+
decode[FooUpdate](
3044+
"""{ "field": { "action": "keep" } }"""
3045+
) match {
3046+
case Left(error) => println(error)
3047+
case Right(patch) => Foo(Some("old-value"), "another-value").patchUsing(patch)
3048+
}
3049+
)
3050+
// expected output:
3051+
// Foo(field = Some(value = "old-value"), anotherField = "another-value")
3052+
```
3053+
3054+
If we cannot modify our API, we have to [choose one semantics for `None` values](supported-patching.md#treating-none-as-no-update-instead-of-set-to-none).
3055+
29513056
## Mixing Scala 2.13 and Scala 3 types
29523057

29533058
[Scala 2.13 project can use Scala 3 artifacts and vice versa](https://docs.scala-lang.org/scala3/guides/migration/compatibility-classpath.html).

0 commit comments

Comments
 (0)