You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OpenStack Keystone API用户更新接口Encoder实现难题

Solution for Handling Partial Updates with Three States in Keystone API Client

I've run into exactly this kind of problem with partial PATCH updates before—needing to distinguish between "leave as-is", "set to a new value", and "clear to null" is surprisingly tricky with standard optional types. Your Updatable ADT is the right way to model this, and we can fix the encoding without relying on risky special values or messy post-processing.

Here's a clean, maintainable solution that directly encodes your Updatable state into the correct JSON structure:

Step 1: Keep Your Updatable ADT (It’s the Right Foundation)

sealed trait Updatable[+T]
case object KeepExistingValue extends Updatable[Nothing]
case object Unset extends Updatable[Nothing]
case class ChangeTo[T](value: T) extends Updatable[T]

Step 2: Update the Update Class to Use Updatable for All Fields

Let’s align all fields with your future-proof goal of using Updatable everywhere:

case class Update(
  name: Updatable[String] = KeepExistingValue,
  password: Updatable[String] = KeepExistingValue,
  defaultProjectId: Updatable[String] = KeepExistingValue,
  enabled: Updatable[Boolean] = KeepExistingValue
)

Step 3: Build a Custom Encoder with Precise Field Control

Instead of relying on auto-derivation with workarounds, we’ll write an encoder that explicitly handles each Updatable field’s state. First, add a helper to convert an Updatable value into an optional JSON field entry:

import io.circe.{Encoder, Json, JsonObject}
import io.circe.syntax._

// Helper to convert an Updatable value to a (field name, Json) pair (or None for keep-as-is)
implicit class UpdatableFieldEncoder[T](val updatable: Updatable[T]) extends AnyVal {
  def toJsonField(fieldName: String)(implicit valueEncoder: Encoder[T]): Option[(String, Json)] =
    updatable match {
      case KeepExistingValue => None          // Don't include the field at all
      case Unset => Some(fieldName -> Json.Null) // Include field with null value
      case ChangeTo(value) => Some(fieldName -> valueEncoder(value)) // Include field with new value
    }
}

Then, write the encoder for your Update class by combining these field entries:

implicit val updateEncoder: Encoder[Update] = Encoder.instance { update =>
  // Collect all non-keep-as-is fields
  val fields = List(
    update.name.toJsonField("name"),
    update.password.toJsonField("password"),
    update.defaultProjectId.toJsonField("default_project_id"), // Match Keystone's snake_case
    update.enabled.toJsonField("enabled")
  ).flatten

  // Convert to JsonObject and then to Json
  JsonObject.fromIterable(fields).asJson
}

Why This Is Better Than Your Previous Solutions

  • No risky special values: We don’t use arbitrary strings to mark states, so there’s zero chance of conflicting with valid user input.
  • Single-pass processing: We build the JSON object directly without needing to filter or modify it after encoding.
  • No dependency on global dropNullValues: This encoder works independently of any global printer settings, so you won’t break other parts of your code that might rely on different JSON behavior.
  • Clear and maintainable: Each field’s behavior is explicitly defined, making it easy to extend or modify later (e.g., adding new fields to Update).

Testing the Three Required Scenarios

Let’s verify this handles all your use cases perfectly:

  1. Keep existing defaultProjectId:
    val update = Update(defaultProjectId = KeepExistingValue)
    updateEncoder(update) // Result: {} (no default_project_id field)
    
  2. Set defaultProjectId to a new value:
    val update = Update(defaultProjectId = ChangeTo("an-id"))
    updateEncoder(update) // Result: {"default_project_id": "an-id"}
    
  3. Clear defaultProjectId (set to null):
    val update = Update(defaultProjectId = Unset)
    updateEncoder(update) // Result: {"default_project_id": null}
    

All three cases align exactly with what the Keystone API expects.

内容的提问来源于stack exchange,提问作者Simão Martins

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 19:23:02