Search code examples
scalacirce

how to convert @JsonAnySetter in jackson to circe


I'm rewriting some java code which is using jackson for json parsing into scala circe.

The java Device class is this -

package forjava;

import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@JsonInclude(Include.NON_NULL)
@JsonPropertyOrder({"ua", "dnt", "ip", "devicetype"})
public class Device implements Serializable {
    @JsonProperty("ua")
    private String ua;
    @JsonProperty("dnt")
    private Integer dnt;
    @JsonProperty("ip")
    private String ip;
    @JsonProperty("devicetype")
    private Integer devicetype;

    @JsonIgnore
    private Map<String, Object> additionalProperties = new HashMap();

    @JsonAnyGetter
    public Map<String, Object> getAdditionalProperties() {
        return this.additionalProperties;
    }

    @JsonAnySetter
    public void setAdditionalProperty(String name, Object value) {
        this.additionalProperties.put(name, value);
    }

    private static final long serialVersionUID = -4938649324295079141L;

    public Device() {
    }

    public Device(String ua, Integer dnt, Integer lmt, String ip, String ipv6, Integer devicetype, String make) {
        this.ua = ua;
        this.dnt = dnt;
        this.ip = ip;
        this.devicetype = devicetype;
    }

    @JsonProperty("ua")
    public String getUa() {
        return this.ua;
    }

    @JsonProperty("ua")
    public void setUa(String ua) {
        this.ua = ua;
    }

    @JsonProperty("dnt")
    public Integer getDnt() {
        return this.dnt;
    }

    @JsonProperty("dnt")
    public void setDnt(Integer dnt) {
        this.dnt = dnt;
    }

    @JsonProperty("ip")
    public String getIp() {
        return this.ip;
    }

    @JsonProperty("ip")
    public void setIp(String ip) {
        this.ip = ip;
    }

    @JsonProperty("devicetype")
    public Integer getDevicetype() {
        return this.devicetype;
    }

    @JsonProperty("devicetype")
    public void setDevicetype(Integer devicetype) {
        this.devicetype = devicetype;
    }

    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(Device.class.getName()).append('@').append(Integer.toHexString(System.identityHashCode(this))).append('[');
        sb.append("ua");
        sb.append('=');
        sb.append(this.ua == null ? "<null>" : this.ua);
        sb.append(',');
        sb.append("dnt");
        sb.append('=');
        sb.append(this.dnt == null ? "<null>" : this.dnt);
        sb.append(',');
        sb.append("ip");
        sb.append('=');
        sb.append(this.ip == null ? "<null>" : this.ip);
        sb.append(',');
        sb.append("devicetype");
        sb.append('=');
        sb.append(this.devicetype == null ? "<null>" : this.devicetype);
        sb.append(',');
        sb.append("additionalProperties");
        sb.append('=');
        sb.append(this.additionalProperties == null ? "<null>" : this.additionalProperties);
        sb.append(',');
        if (sb.charAt(sb.length() - 1) == ',') {
            sb.setCharAt(sb.length() - 1, ']');
        } else {
            sb.append(']');
        }
        return sb.toString();
    }

    public int hashCode() {
        int result = 1;

        result = result * 31 + (this.ua == null ? 0 : this.ua.hashCode());
        result = result * 31 + (this.devicetype == null ? 0 : this.devicetype.hashCode());
        result = result * 31 + (this.ip == null ? 0 : this.ip.hashCode());
        result = result * 31 + (this.dnt == null ? 0 : this.dnt.hashCode());
        return result;
    }

    public boolean equals(Object other) {
        if (other == this) {
            return true;
        } else if (!(other instanceof Device)) {
            return false;
        } else {
            Device rhs = (Device)other;
            return (Objects.equals(this.ua, rhs.ua))
                    && (Objects.equals(this.devicetype, rhs.devicetype) || this.devicetype != null
                    && this.devicetype.equals(rhs.devicetype))
                    && (Objects.equals(this.ip, rhs.ip))
                    && (Objects.equals(this.dnt, rhs.dnt)) ;
        }
    }
}

Below is the java code to parsing the json -

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class DeviceJsonParserDemo {
    public static void main(String[] args) throws IOException {

        ObjectMapper jsonMapperBidRequest  = new ObjectMapper();
        String dev = Files.readString(Paths.get("src/main/resources/device.json"));
        Device device = jsonMapperBidRequest.readValue(dev, Device.class);
        System.out.println(device);
    }
}

When you run the above code you will get the below output -

Device@5876a9af[ua=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36,dnt=<null>,ip=<null>,devicetype=<null>,additionalProperties={started=now, devtime=now}]

As you notice in the above output, the fields that are not in Device class are in the additionalProperties field.

Now I want to migrate the above logic into typelevel circe

I have below Decoded

import io.circe.{Decoder, HCursor}

case class Device (ua: String, dnt: Option[Int], ip: Option[String], devicetype: Option[Int], addlProperties: Map[String, Any] = Map.empty)

object Device {
  implicit val decodeFoo: Decoder[Device] = new Decoder[Device] {
    final def apply(c: HCursor): Decoder.Result[Device] =
      for {
        ua <- c.downField("ua").as[String]
        dnt <- c.downField("dnt").as[Option[Int]]
        ip <- c.downField("ip").as[Option[String]]
        devicetype <- c.downField("devicetype").as[Option[Int]]
      } yield new Device(ua, dnt, ip, devicetype)
  }
}

The java parsing logic is

import scala.io.Source
import io.circe.parser._

object DeviceJsonCirceDecodeDemo extends App {
  val devString = Source.fromFile("src/main/resources/device.json").mkString
  val device = decode[Device](devString)
  println(device)
}

When I run the above code I get the below output -

Right(Device(Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36,None,None,None,Map()))

As you see here, the addlProperties map is empty because it doesn't have decoder.

The code is in github here for java and for scala here How can I achieve the same in circe.


Solution

  • A relatively simple if somewhat awkward to maintain solution is to decode the full object as a Map and removing the known keys.

    Your decoder would become:

    implicit val decodeFoo: Decoder[Device] = Decoder.instance(c =>
      for {
        ua <- c.get[String]("ua")
        dnt <- c.get[Option[Int]]("dnt")
        ip <- c.get[Option[String]]("ip")
        devicetype <- c.get[Option[Int]]("devicetype")
        additional <- c.as[Map[String, Json]].map(_ - "ua" - "dnt" - "ip" - "devicetype")
      } yield new Device(ua, dnt, ip, devicetype, additional)
    )
    

    I've replaced new Decoder with Decoder.instance and c.downField(...).as[...] with c.get[...](...). This also assumes addlProperties is a Map[String, Json], you might want to define a decoder for a specific set of types.