【问题标题】:How to parse a JSON object (which is a list) into simple Scala class objects in scala?如何将 JSON 对象(这是一个列表)解析为 scala 中的简单 Scala 类对象?
【发布时间】:2021-03-12 05:14:06
【问题描述】:

我花了太多时间尝试完成这项工作,我是 Scala 新手。

基本上我向 API 发出请求并得到以下响应:

[
  {
    "id": "bde585ea-43ad-4e62-9f20-ea721193e0a5",
    "clientId": "account",
    "realm":"test-realm-uqrw"
    "name": "${client_account}",
    "rootUrl": "${authBaseUrl}",
    "baseUrl": "/realms/test-realm-uqrw/account/",
    "surrogateAuthRequired": false,
    "enabled": true,
    "alwaysDisplayInConsole": false,
    "clientAuthenticatorType": "client-secret",
    "defaultRoles": [
      "manage-account",
      "view-profile"
    ],
    "redirectUris": [
      "/realms/test-realm-uqrw/account/*"
    ],
    "webOrigins": [],
    "protocol": "openid-connect",
    "attributes": {},
    "authenticationFlowBindingOverrides": {},
    "fullScopeAllowed": false,
    "nodeReRegistrationTimeout": 0,
    "defaultClientScopes": [
      "web-origins",
      "role_list",

    ],

    "access": {
      "view": true,
      "configure": true,
      "manage": true
    }
  },
  {..another object of the same type, different values },
  {..another object of the same type, different values }
]

我只需要从任何这些对象中提取"id" 字段(稍后我将通过realm 属性匹配)。有没有一种简单的方法可以将该 json 列表转换为 List[]Map[String, Any]?我之所以说Any,是因为值的类型多种多样——布尔值、字符串、映射、列表。

我已经尝试了几种方法(内部工具)和 Jackson(错误:com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of scala.collection.immutable.List(no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information),我得到的最接近的是Tuples 给我的奇怪列表结果不正确(因为处理不正确)。

有什么简单的方法可以做到这一点?还是我注定要为此 API 响应创建一个自定义类?或者我可以直接遍历这个 JSON 文档(我只想从该数组中的 一个 对象中获取 一个 值)并提取该值吗?

【问题讨论】:

  • Jackson上面有很多Scala库,帮助Scala集成json用例:Circe、Json4s、Argonaut、LiftJson、Playjson

标签: json scala jackson


【解决方案1】:

原生和现代之一是Circe,在您的情况下,解决方案可能类似于:

import io.circe._, io.circe.parser._, io.circe.generic.auto._, io.circe.syntax._

case class Response(
    id: String,
    clientId: String,
    realm: String,
    name: String,
    rootUrl: String,
    baseUrl: String,
    surrogateAuthRequired: Boolean,
    enabled: Boolean,
    alwaysDisplayInConsole: Boolean,
    clientAuthenticatorType: String,
    defaultRoles: List[String],
    redirectUris: List[String],
    webOrigins: List[String],
    protocol: String,
    fullScopeAllowed: Boolean,
    nodeReRegistrationTimeout: Int,
    defaultClientScopes: List[String],
    access: Access
)

case class Access(view: Boolean, configure: Boolean, manage: Boolean)

val json =
  s"""
       |[
       |  {
       |    "id": "bde585ea-43ad-4e62-9f20-ea721193e0a5",
       |    "clientId": "account",
       |    "realm":"test-realm-uqrw",
       |    "name": "client_account",
       |    "rootUrl": "authBaseUrl",
       |    "baseUrl": "/realms/test-realm-uqrw/account/",
       |    "surrogateAuthRequired": false,
       |    "enabled": true,
       |    "alwaysDisplayInConsole": false,
       |    "clientAuthenticatorType": "client-secret",
       |    "defaultRoles": [
       |      "manage-account",
       |      "view-profile"
       |    ],
       |    "redirectUris": [
       |      "/realms/test-realm-uqrw/account/*"
       |    ],
       |    "webOrigins": [],
       |    "protocol": "openid-connect",
       |    "fullScopeAllowed": false,
       |    "nodeReRegistrationTimeout": 0,
       |    "defaultClientScopes": [
       |      "web-origins",
       |      "role_list"
       |    ],
       |
       |    "access": {
       |      "view": true,
       |      "configure": true,
       |      "manage": true
       |    }
       |  }
       |]
       |""".stripMargin

println(parse(json).flatMap(_.as[List[Response]]))

将打印输出:

Right(List(Response(bde585ea-43ad-4e62-9f20-ea721193e0a5,account,test-realm-uqrw,client_account,authBaseUrl,/realms/test-realm-uqrw/account/,false,true,false,client-secret,List(manage-account, view-profile),List(/realms/test-realm-uqrw/account/*),List(),openid-connect,false,0,List(web-origins, role_list),Access(true,true,true))))

斯卡蒂:https://scastie.scala-lang.org/5OpAUTjSTEWWTrH4X24vAg

最大的优势 - 与 Jackson 不同,它不是基于运行时反射,而是基于编译时派生。

更新

正如@LuisMiguelMejíaSuárez 在 cmets 部分中正确建议的那样,如果您只想获取 id 字段,您可以在不进行完整模型解析的情况下进行,例如:

import io.circe._, io.circe.parser._

val json =
  s"""
       |[
       |  {
       |    "id": "bde585ea-43ad-4e62-9f20-ea721193e0a5"
       |  },
       |  {
       |    "id": "bde585ea-43ad-4e62-9f20-ea721193e0a6"
       |  }
       |]
       |""".stripMargin

println(parse(json).map(_.hcursor.values.map(_.map(_.hcursor.downField("id").as[String]))))

打印出来:

Right(Some(Vector(Right(bde585ea-43ad-4e62-9f20-ea721193e0a5), Right(bde585ea-43ad-4e62-9f20-ea721193e0a6))))

斯卡蒂:https://scastie.scala-lang.org/bSSZdLPyTJWcup2KIb4zAw

但要小心 - 手动 JSON 操作,通常在边缘情况下使用。我建议即使对于简单的情况也使用模型推导。

【讨论】:

  • @LuisMiguelMejíaSuárez 非常感谢,很好,我错过了只需要一个字段,我会编辑我的答案。
【解决方案2】:

使用jsoniter-scala FTW!

它在推导中很方便,在运行时效率最高。提取 JSON 值是它最出彩的地方。

请添加以下依赖项:

libraryDependencies ++= Seq(
  // Use the %%% operator instead of %% for Scala.js  
  "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core"   % "2.6.4",
  // Use the "provided" scope instead when the "compile-internal" scope is not supported  
  "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.6.4" % "compile-internal"
)

那么对于 id 值,无需为其他值定义字段或数据结构。

只需定义一个最简单的数据结构并立即解析:

import com.github.plokhotnyuk.jsoniter_scala.macros._
import com.github.plokhotnyuk.jsoniter_scala.core._
import java.util.UUID

val json = """[
             |  {
             |    "id": "bde585ea-43ad-4e62-9f20-ea721193e0a5",
             |    "clientId": "account",
             |    "realm":"test-realm-uqrw",
             |    "name": "${client_account}",
             |    "rootUrl": "${authBaseUrl}",
             |    "baseUrl": "/realms/test-realm-uqrw/account/",
             |    "surrogateAuthRequired": false,
             |    "enabled": true,
             |    "alwaysDisplayInConsole": false,
             |    "clientAuthenticatorType": "client-secret",
             |    "defaultRoles": [
             |      "manage-account",
             |      "view-profile"
             |    ],
             |    "redirectUris": [
             |      "/realms/test-realm-uqrw/account/*"
             |    ],
             |    "webOrigins": [],
             |    "protocol": "openid-connect",
             |    "attributes": {},
             |    "authenticationFlowBindingOverrides": {},
             |    "fullScopeAllowed": false,
             |    "nodeReRegistrationTimeout": 0,
             |    "defaultClientScopes": [
             |      "web-origins",
             |      "role_list",
             |
             |    ],
             |
             |    "access": {
             |      "view": true,
             |      "configure": true,
             |      "manage": true
             |    }
             |  }
             |]""".stripMargin.getBytes("UTF-8")

case class Response(id: UUID)

implicit val codec: JsonValueCodec[List[Response]] = JsonCodecMaker.make

val responses = readFromArray(json)

println(responses)
println(responses.map(_.id))

预期输出:

List(Response(bde585ea-43ad-4e62-9f20-ea721193e0a5))
List(bde585ea-43ad-4e62-9f20-ea721193e0a5)

如果需要以不同方式或更有效地处理您的数据,请随时在此处或gitter chat 寻求帮助。

【讨论】:

    【解决方案3】:

    你可以使用play json,很简单。

    import play.api.libs.json._
    
    case class Access(view: Boolean, configure: Boolean, manage: Boolean)
    case class Response(
        id: String,
        clientId: String,
        realm: String,
        name: String,
        rootUrl: String,
        baseUrl: String,
        surrogateAuthRequired: Boolean,
        enabled: Boolean,
        alwaysDisplayInConsole: Boolean,
        clientAuthenticatorType: String,
        defaultRoles: List[String],
        redirectUris: List[String],
        webOrigins: List[String],
        protocol: String,
        fullScopeAllowed: Boolean,
        nodeReRegistrationTimeout: Int,
        defaultClientScopes: List[String],
        access: Access
    )
    
    
    
    val string =
      s"""
           |[
           |  {
           |    "id": "bde585ea-43ad-4e62-9f20-ea721193e0a5",
           |    "clientId": "account",
           |    "realm":"test-realm-uqrw",
           |    "name": "client_account",
           |    "rootUrl": "authBaseUrl",
           |    "baseUrl": "/realms/test-realm-uqrw/account/",
           |    "surrogateAuthRequired": false,
           |    "enabled": true,
           |    "alwaysDisplayInConsole": false,
           |    "clientAuthenticatorType": "client-secret",
           |    "defaultRoles": [
           |      "manage-account",
           |      "view-profile"
           |    ],
           |    "redirectUris": [
           |      "/realms/test-realm-uqrw/account/*"
           |    ],
           |    "webOrigins": [],
           |    "protocol": "openid-connect",
           |    "fullScopeAllowed": false,
           |    "nodeReRegistrationTimeout": 0,
           |    "defaultClientScopes": [
           |      "web-origins",
           |      "role_list"
           |    ],
           |
           |    "access": {
           |      "view": true,
           |      "configure": true,
           |      "manage": true
           |    }
           |  }
           |]
           |""".stripMargin
    
    implicit val ac = Json.format[Access]
    implicit val res = Json.format[Response]
    
    println(Json.parse(string).asInstanceOf[JsArray].value.map(_.as[Response])) 
    

    避免异常-

    val responseOpt = Json.parse(string) match {
            case JsArray(value: collection.IndexedSeq[JsValue]) => value.map(_.asOpt[Response])
            case _ => Seq.empty
          }
    

    见:https://scastie.scala-lang.org/RBUHhxxIQAGcKgk9a9iwIA

    这是文档:https://www.playframework.com/documentation/2.8.x/ScalaJson

    【讨论】:

    • 我可以让Response 类只保存两个字段 - idrealm - 并丢弃其他字段吗?解析会处理吗?
    • @Saturnian 是的,只需从响应案例类中删除其他字段,请参阅 scastie.scala-lang.org/JNMH6IiAR0qq42FVbB4VGg 。当您将案例类中的任何字段声明为非选项类型时,它只会产生问题,但该字段不存在于 json Object 中。
    • 我很好奇,为什么 res 是隐含的?
    • @Saturnian 这个隐式将被宏用于根据响应案例类读取和写入 json。这就是为什么它不基于运行时反射。在编译时,它将基于隐式创建所有 json 映射人员(代码)。
    • 这段代码超级不安全。 asInstanceOfas 都可能抛出异常。
    【解决方案4】:

    使用 play-json 的另一个选项是定义路径:

    val jsPath = JsPath \\ "id"
    

    然后应用它:

    jsPath(Json.parse(jsonString))
    

    代码在Scastie 运行。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-01-18
      • 1970-01-01
      • 2020-12-15
      • 1970-01-01
      • 2021-01-01
      • 2013-03-08
      • 1970-01-01
      • 2019-11-26
      相关资源
      最近更新 更多