@@ -2948,6 +2948,111 @@ fields from the patching:
2948
2948
// Foo(a = "a", b = "d")
2949
2949
```
2950
2950
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
+
2951
3056
## Mixing Scala 2.13 and Scala 3 types
2952
3057
2953
3058
[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