Skip to content

Handling colons in property names #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
ilkkapoutanen-61n opened this issue Apr 9, 2025 · 10 comments
Open

Handling colons in property names #53

ilkkapoutanen-61n opened this issue Apr 9, 2025 · 10 comments

Comments

@ilkkapoutanen-61n
Copy link

I'm having a bit of a problem with a JSON schema that gets automatically generated from an XML schema (don't ask). Due to the XML heritage, the schema has objects where some properties have names like xmlns:whatever. When I generate Kotlin classes from that schema, the property name does get backticks around it, but unfortunately that is not enough in the case of colons and a few other special characters, as documented in section 1.2.4 of the language spec.

As I am using the Gradle plugin, I don't see a way to work around this problem with the config. If a custom class template could be provided by me, I guess I could just swap kotlinName for javaName when rendering properties. Anyway, since a name with colons etc can't be a legal Kotlin identifier on the JVM, maybe checkKotlinName() on the NamedConstraints companion object could just replace all the characters that the language spec rules out with underscores? Then I could still use annotation templating to add annotations for my favorite JSON parser library using the original, un-mangled names and everything would be dandy.

@pwall567
Copy link
Owner

Hi @ilkkapoutanen-61n , thanks for this. Now you've pointed it out, I'm surprised no-one has reported this issue before.

Your suggestion of replacing illegal characters with underscore is a good one; I should be able to get a version with this change out to you in a couple of days.

@pwall567
Copy link
Owner

pwall567 commented Apr 13, 2025

I decided on a slight variation to your suggestion. I was concerned that if all illegal characters mapped to underscore there was a possibility of clashes. For example, two properties named size>average and size<average would both map to the same name.

The kotlinName property now maps illegal characters to underscore, followed by the 2-digit hex value of the character, for example size_3Eaverage.

I hope the resolves your issue – the new version is 0.119

@pwall567
Copy link
Owner

pwall567 commented Apr 13, 2025

And you mentioned using annotations with your "favorite JSON parser library" – is that kjson?

If not, perhaps you might like to take a look – it's a pure Kotlin implementation (which means it respects Kotlin nullability), it's very easy to use and it covers all the standard Kotlin classes. Your config file would need to contain:

annotations:
  fields:
    io.kjson.annotation.JSONName: '"{{&name}}"'

Note the single quotes to tell YAML that the double quotes are to be part of the value, and double quotes to tell Kotlin that the expanded template is a string.

And in your Gradle build file:

    implementation("io.kjson:kjson:9.7")
    implementation("io.kjson:kjson-annotations:1.5")

Give it a try if you haven't already 😄

@ilkkapoutanen-61n
Copy link
Author

Thanks, that solves that naming problem nicely! I then hit another one though: given this schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "Widget": {
      "$ref": "#/$defs/Widget"
    }
  },
  "$defs": {
    "Widget": {
      "type": "object",
      "properties": {
        "isFrobbed": {
          "type": "boolean"
        }
      }
    }
  }
}

and using the defaults yields this output:

data class NamingClashTest(
    val Widget: Widget? = null
) {

    data class Widget(
        val isFrobbed: Boolean? = null
    )

}

Now the Widget name clashes between the outer data class property and the nested data class name. There is classNames in the config which I think I can use to solve this, especially as this problem only seems to crop up about a dozen times and I don't see these particular schemata changing often. Anyway, maybe it's a pattern that the generator could also handle differently in the future?

Anyway, thanks, I can keep forging ahead now. As for the JSON de/ser library, there's a lot of Moshi in this codebase so that was my first choice, but as a matter of fact I ran into a problem with that too :D it seems like it doesn't like number-valued enums and failed to parse correct data. I might give kjson a go, thanks for that!

@pwall567
Copy link
Owner

OK, so I made an arbitrary decision to capitalise the property name or the $ref name to create the nested class, but I didn't ensure that the property name was not already capitalised. I'm reluctant to change something that's been in place for several years, but I guess any cases like your example would fail to compile anyway, so changing the behaviour shouldn't be a problem. Let me think about that, but in the meantime you have a workaround using classNames.

And kudos for using the irregular plural of schema 😄

@pwall567
Copy link
Owner

And BTW, if kjson doesn't handle the case you're having difficulty with, let me know – if it's failing on a case that it should be able to handle I'd like to know that.

@pwall567
Copy link
Owner

OK, I've fixed the case of the nested class name clashing with the property name. Now, if the names are the same, the first letter of the property name is converted to lower case (it can only happen if it was originally upper case).

The new version is 0.120; I might have just saved you from having to do a lot of classNames entries.

@emibla
Copy link

emibla commented May 20, 2025

Hello, @pwall567!

I see here that the behavior of the generator is changed in 0.120 to uncapitalize the property names in cases where the name clashes with the class name. This is a problem for me, as I need the properties to be serializable with the exact name they had in the json schema (by programmatically adding @SerialName after-the-fact).

Is there a work-around for this? Perhaps an option for disabling that the generator modifies the naming of the properties?

Thank you for a great library! This is heavily appreciated 😇 Hoping for a quick fix for this problem:))

@pwall567
Copy link
Owner

Hi @emibla , you say you are programmatically adding @SerialName – are you aware that you can add annotations with a configuration option?

In your case, it would be:

annotations:
  fields:
    kotlinx.serialization.SerialName: '"{{&name}}"'

As explained in an earlier comment, the single quotes tell YAML that the double quotes are to be part of the value, and double quotes tell Kotlin that the expanded template is a string.

And the name property substituted in is the original name, before any upper or lower case conversion, which should (I hope) resolve your issue.

@emibla
Copy link

emibla commented May 22, 2025

Thank you for the swift response @pwall567 , that worked perfectly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants