scalamonocle-scala

Scala: how to upsert field value in Monocle


Given the JsonExample in the monocle project, I would like to create a lens where a set call will either replace a value in a key/value pair, or create the key/value pair if it doesnt already exist.

However this seems to represented with either an index (which can compose type safe) or an at, which does not type safe

//for replacing:
(jsObject composeOptional index("age") composePrism jsNumber).set(45)

//for creating:
(jsObject composeLens at("age")).set(JsNumber(45)) <- will accept any old json

Is what I am after possible?
Also could I extend it, such that if age was nested in another JsObject, for example:

val n = (jsObject composeOptional index("nested") composePrism 
jsObject composeOptional index("age") composePrism jsNumber).set(45)

Where the key/value pair for "nested" didnt yet exist, that it would create the object at nested and then add the field

n(JsObject(Map.empty)) -> JsObject(Map("nested" -> JsObject("age" -> JsNumber(45)))

Solution

  • let's have a look at index and at signature for JsObject:

    def at(field: String): Lens[JsObject, Option[Json]]
    def index(field: String): Optional[JsObject, Json]
    

    at is a Lens so its target ('Option[Json]') is always present. It means that we can add, delete and update the Json element at any field of a JsonObject.

    import argonaut._, Argonaut._
    import monocle.function._
    
    (jObjectPrism composeLens at("name")).set(Some(jString("John")))(Json())
    > res0: argonaut.Json = {"name":"John"}
    
    (jObjectPrism composeLens at("name")).set(Some(jString("Robert")))(res0)
    > res1: argonaut.Json = {"name":"Robert"}
    
    (jObjectPrism composeLens at("name")).set(None)(res0)
    > res2: argonaut.Json = {}
    

    On the other hand, index is an Optional so it is target (Json) may or may not be there. It means that index can only update values but cannot add or delete.

    (jObjectPrism composeLens index("name")).set(jString("Robert"))(Json())
    > res3: argonaut.Json = {}
    
    (jObjectPrism composeLens index("name")).set(jString("Robert"))(res0)
    > res4: argonaut.Json = {"name":"Robert"}
    

    So to come back to your original question, if you want to add or update value at a particular field, you need to use at and wrap the Json in a Some (see res1), it will overwrite or create the Json at that field.