charleskorn / kaml

YAML support for kotlinx.serialization
Apache License 2.0
489 stars 46 forks source link

Peeking, or working with subsections of YAML #581

Open nomisRev opened 1 week ago

nomisRev commented 1 week ago

Describe the problem you'd like to solve

When parsing OpenAPI Specifications, you need to peek or be able to interact with subsections of the YAML. For example for Reference<T> where you don't know if it's an inline defined schema, or a referenced one.

schema:
  $ref: "#/components/schemas/Pets"

vs

schemas:
  Pet:
    required:
      - id
      - name
    properties:
      id:
        type: integer
        format: int64
      name:
        type:
          - string
          - integer
      tag:
        type: string

This needs to be parsed into a ReferenceOr<Schema> sealed class, with without any type markers available in the yaml.

Describe the solution you'd like

In JSON I am currently doing this:

override fun deserialize(decoder: Decoder): ReferenceOr<T> =
  when (decoder) {
    is JsonDecoder -> deserializeJson(decoder)
    is YamlInput -> deserializeYaml(decoder)
    else -> throw UnsupportedOperationException("Unsupported decoder: $decoder")
  }

private fun deserializeJson(decoder: JsonDecoder): ReferenceOr<T> {
  val json = decoder.decodeSerializableValue(JsonElement.serializer())
  return if ((json as JsonObject).contains(RefKey))
    Reference(json[RefKey]!!.jsonPrimitive.content)
  else Value(decoder.json.decodeFromJsonElement(dataSerializer, json))
}

Describe alternatives you've considered

I am not entirely sure yet if it's not possible, but I am a bit stuck while still exploring the API. It seems that you cannot ask a YamlInput to be decoded to YamlNode, like I do with Json.

This is because YamlNode itself is not a @Serializable, but there is a separate parser build for it instead. I am not entirely sure how hard, or easy, it could be to bridge this functionality. Being able to do YamlInput::decodeYamlNode would also be sufficient, since that would allow me to peek in a similar way than Json.

Additional context

If I find a solution, or a straightforward way to improve this inside the library I will update this issue and try to contribute if makes sense.

OptimumCode commented 1 week ago

Hi, @nomisRev.

Maybe this is what you are looking for (I found it here in tests). This serializer takes a node from YamlInput and works with it

private object DecodingFromYamlNodeSerializer : KSerializer<DatabaseListing> {
    override fun deserialize(decoder: Decoder): DatabaseListing {
        check(decoder is YamlInput)

        val currentMap = decoder.node.yamlMap.get<YamlMap>("databaseListing")
        checkNotNull(currentMap)

        val list = currentMap.entries.map { (_, value) ->
            decoder.yaml.decodeFromYamlNode(Database.serializer(), value)
        }

        return DatabaseListing(list)
    }
}
nomisRev commented 1 week ago

That's indeed what I was trying to do, or at least one of my attempts, but I guess I just didn't take it far enough 😁 The path is more complex than databaseListing, but should be calculate-able.

I guess getCurrentPath().toHumanReadableString() could work, but it's a bit unsafe because those could contain List, etc although in this case it shouldn't. This use-case is specifically to OpenAPI type, not format support in the generated clients.

Thanks for the hint, I'll give that a shot later. For the moment I am using a tiny workaround for moving more quickly.