pdvrieze / xmlutil

XML Serialization library for Kotlin
https://pdvrieze.github.io/xmlutil/
Apache License 2.0
363 stars 30 forks source link

UnknownXmlFieldException parsing openlyrics when namespace is specified #194

Closed odifek closed 5 months ago

odifek commented 5 months ago

Thanks for this awesome tool. I am trying to parse openlyrics but keep getting this error

nl.adaptivity.xmlutil.serialization.UnknownXmlFieldException: Could not find a field for name (verse) verse/{http://openlyrics.info/namespace/2009/song}lines (Element)
  candidates: name (Attribute), lines (Element) at position Line number = 17

Here is the model

@Serializable
@SerialName("song")
@XmlSerialName(value = "song")
public data class OpenLyricsSong(
    // Metadata
    val lang: String = "en",
    val chordNotation: String = "english",
    val version: String = "0.9", // open lyrics version
    val createdIn: String? = null,
    val modifiedIn: String? = null,
    val modifiedDate: String? = null,

    val properties: Properties,
    @XmlChildrenName("verse")
    val lyrics: List<Verse>,
) {
    @Serializable
    @SerialName("properties")
    public data class Properties(
        @XmlChildrenName("title")
        val titles: List<Title>,
        @XmlChildrenName("author")
        val authors: List<Author>? = null,
        @XmlChildrenName("songbook")
        val songbooks: List<Songbook>? = null,
        @XmlElement
        val verseOrder: String? = null,
        @XmlElement
        val keywords: String? = null,
        @XmlChildrenName("theme")
        val themes: List<Theme>? = null,
        @XmlChildrenName("comment")
        val comments: List<String>? = null,
        @XmlElement
        val copyright: String? = null,
        @XmlElement
        val publisher: String? = null,
    )

    @Serializable
    public data class Author(
        @XmlValue val value: String,
        val type: String? = null,
        val comment: String? = null,
    )

    @Serializable
    public data class Title(
        @XmlValue val value: String,
        val lang: String? = null,
        val original: Boolean? = null,
        val translit: String? = null, // transliteration language
    )

    @Serializable
    public data class Songbook(
        val name: String,
        val entry: String? = null,
    )

    @Serializable
    public data class Theme(@XmlValue val name: String, val lang: String? = null)

    @Serializable
    @XmlSerialName("verse")
    @SerialName("verse")
    public data class Verse(
        val name: String,
        val lines: List<Lines>,
    )

    @Serializable
    @SerialName("lines")
    public data class Lines(
        @XmlValue val content: String,
        val part: String? = null, // e.g. men, women, alto (etc)
    )
}

Sample song

const val SAMPLE_SONG = """<?xml version="1.0" encoding="utf-8"?>
<song createdIn="python3.6 and xmlwitch" modifiedDate="2018-07-29T23:59:59" version="0.8" xmlns="http://openlyrics.info/namespace/2009/song">
  <properties>
    <titles>
      <title>'Tis so sweet to trust in Jesus</title>
    </titles>
    <verseOrder>v1 c1 v2 c1 v3 c1 v4 c1</verseOrder>
    <authors>
      <author>Louisa M.</author>
    </authors>
    <songbooks>
      <songbook entry="122" name="Watchman (HSCF)" />
    </songbooks>
  </properties>
  <lyrics>
    <verse name="v1">
      <lines>'Tis so sweet to trust in Jesus,
And to take Him at His Word;
Just to rest upon His promise,
And to know, Thus says the Lord!</lines>
    </verse>
    <verse name="c1">
      <lines>Jesus, Jesus, how I trust Him!
How I've proved Him o'er and o'er
Jesus, Jesus, precious Jesus!
O for grace to trust Him more!</lines>
    </verse>
    <verse name="v2">
      <lines>O how sweet to trust in Jesus,
Just to trust His cleansing blood;
And in simple faith to plunge me
'Neath the healing, cleansing flood!</lines>
    </verse>
    <verse name="v3">
      <lines>Yes, 'tis sweet to trust in Jesus,
Just from sin and self to cease;
Just from Jesus simply taking
Life and rest, and joy and peace.</lines>
    </verse>
    <verse name="v4">
      <lines>I'm so glad I learned to trust Thee,
Precious Jesus, Savior, friend;
And I know that Thou art with me,
Wilt be with me to the end.</lines>
    </verse>
  </lyrics>
</song>
"""

This is how I decode it

 val openLyricsSong = xml.decodeFromString(
                    deserializer = OpenLyricsSong.serializer(),
                    string = lyricsXmlContent
                        .replace("<br/>", "\\n"),
                    rootName = QName("http://openlyrics.info/namespace/2009/song", "song")
)

But it works when I remove the namespace and rootName

 val openLyricsSong = xml.decodeFromString(
                    deserializer = OpenLyricsSong.serializer(),
                    string = lyricsXmlContent
                        .replace("<br/>", "\\n")
                        .replace("xmlns=\"([^\"]+)\"".toRegex(), ""),
)

Here is the XML builder

val xml = XML {
            xmlVersion = XmlVersion.XML10
            xmlDeclMode = XmlDeclMode.Auto
            indentString = "  "
            repairNamespaces = true
        }

Is there anything I am missing?

pdvrieze commented 5 months ago

There appears to be some interaction with the automatic namespace guessing code together with the (eluded) lines list. Probably the easiest way is to explicitly specify the namespace annotation parameter.

odifek commented 5 months ago

Indeed! It worked after I specified namespace like thus.

@XmlSerialName("lines", namespace = "http://openlyrics.info/namespace/2009/song")
    public data class Lines

Thank you

odifek commented 5 months ago

I will mark the issue closed now. Thanks