<lambda>null1 package kotlinx.serialization.json
2 
3 import kotlinx.serialization.*
4 import kotlinx.serialization.descriptors.*
5 
6 
7 /**
8  * Represents naming strategy — a transformer for serial names in a [Json] format.
9  * Transformed serial names are used for both serialization and deserialization.
10  * A naming strategy is always applied globally in the Json configuration builder
11  * (see [JsonBuilder.namingStrategy]).
12  *
13  * Actual transformation happens in the [serialNameForJson] function.
14  * It is possible to apply additional filtering inside the transformer using the `descriptor` parameter in [serialNameForJson].
15  *
16  * Original serial names are never used after transformation, so they are ignored in a Json input.
17  * If the original serial name is present in the Json input but transformed is not,
18  * [MissingFieldException] still would be thrown. If one wants to preserve the original serial name for deserialization,
19  * one should use the [JsonNames] annotation, as its values are not transformed.
20  *
21  * ### Common pitfalls in conjunction with other Json features
22  *
23  * * Due to the nature of kotlinx.serialization framework, naming strategy transformation is applied to all properties regardless
24  * of whether their serial name was taken from the property name or provided by @[SerialName] annotation.
25  * Effectively, it means one cannot avoid transformation by explicitly specifying the serial name.
26  *
27  * * Collision of the transformed name with any other (transformed) properties serial names or any alternative names
28  * specified with [JsonNames] will lead to a deserialization exception.
29  *
30  * * Naming strategies do not transform serial names of the types used for the polymorphism, as they always should be specified explicitly.
31  * Values from [JsonClassDiscriminator] or global [JsonBuilder.classDiscriminator] also are not altered.
32  *
33  * ### Controversy about using global naming strategies
34  *
35  * Global naming strategies have one key trait that makes them a debatable and controversial topic:
36  * They are very implicit. It means that by looking only at the definition of the class,
37  * it is impossible to say which names it will have in the serialized form.
38  * As a consequence, naming strategies are not friendly to refactorings. Programmer renaming `myId` to `userId` may forget
39  * to rename `my_id`, and vice versa. Generally, any tools one can imagine work poorly with global naming strategies:
40  * Find Usages/Rename in IDE, full-text search by grep, etc. For them, the original name and the transformed are two different things;
41  * changing one without the other may introduce bugs in many unexpected ways.
42  * The lack of a single place of definition, the inability to use automated tools, and more error-prone code lead
43  * to greater maintenance efforts for code with global naming strategies.
44  * However, there are cases where usage of naming strategies is inevitable, such as interop with an existing API or migrating a large codebase.
45  * Therefore, one should carefully weigh the pros and cons before considering adding global naming strategies to an application.
46  */
47 @ExperimentalSerializationApi
48 public fun interface JsonNamingStrategy {
49     /**
50      * Accepts an original [serialName] (defined by property name in the class or [SerialName] annotation) and returns
51      * a transformed serial name which should be used for serialization and deserialization.
52      *
53      * Besides string manipulation operations, it is also possible to implement transformations that depend on the [descriptor]
54      * and its element (defined by [elementIndex]) currently being serialized.
55      * It is guaranteed that `descriptor.getElementName(elementIndex) == serialName`.
56      * For example, one can choose different transformations depending on [SerialInfo]
57      * annotations (see [SerialDescriptor.getElementAnnotations]) or element optionality (see [SerialDescriptor.isElementOptional]).
58      *
59      * Note that invocations of this function are cached for performance reasons.
60      * Caching strategy is an implementation detail and should not be assumed as a part of the public API contract, as it may be changed in future releases.
61      * Therefore, it is essential for this function to be pure: it should not have any side effects, and it should
62      * return the same String for a given [descriptor], [elementIndex], and [serialName], regardless of the number of invocations.
63      */
64     public fun serialNameForJson(descriptor: SerialDescriptor, elementIndex: Int, serialName: String): String
65 
66     /**
67      * Contains basic, ready to use naming strategies.
68      */
69     @ExperimentalSerializationApi
70     public companion object Builtins {
71 
72         /**
73          * A strategy that transforms serial names from camel case to snake case — lowercase characters with words separated by underscores.
74          * The descriptor parameter is not used.
75          *
76          * **Transformation rules**
77          *
78          * Words' bounds are defined by uppercase characters. If there is a single uppercase char, it is transformed into lowercase one with underscore in front:
79          * `twoWords` -> `two_words`. No underscore is added if it was a beginning of the name: `MyProperty` -> `my_property`. Also, no underscore is added if it was already there:
80          * `camel_Case_Underscores` -> `camel_case_underscores`.
81          *
82          * **Acronyms**
83          *
84          * Since acronym rules are quite complex, it is recommended to lowercase all acronyms in source code.
85          * If there is an uppercase acronym — a sequence of uppercase chars — they are considered as a whole word from the start to second-to-last character of the sequence:
86          * `URLMapping` -> `url_mapping`, `myHTTPAuth` -> `my_http_auth`. Non-letter characters allow the word to continue:
87          * `myHTTP2APIKey` -> `my_http2_api_key`,  `myHTTP2fastApiKey` -> `my_http2fast_api_key`.
88          *
89          * **Note on cases**
90          *
91          * Whether a character is in upper case is determined by the result of [Char.isUpperCase] function.
92          * Lowercase transformation is performed by [Char.lowercaseChar], not by [Char.lowercase],
93          * and therefore does not support one-to-many and many-to-one character mappings.
94          * See the documentation of these functions for details.
95          */
96         @ExperimentalSerializationApi
97         public val SnakeCase: JsonNamingStrategy = object : JsonNamingStrategy {
98             override fun serialNameForJson(
99                 descriptor: SerialDescriptor,
100                 elementIndex: Int,
101                 serialName: String
102             ): String = convertCamelCase(serialName, '_')
103 
104             override fun toString(): String = "kotlinx.serialization.json.JsonNamingStrategy.SnakeCase"
105         }
106 
107         /**
108          * A strategy that transforms serial names from camel case to kebab case — lowercase characters with words separated by dashes.
109          * The descriptor parameter is not used.
110          *
111          * **Transformation rules**
112          *
113          * Words' bounds are defined by uppercase characters. If there is a single uppercase char, it is transformed into lowercase one with a dash in front:
114          * `twoWords` -> `two-words`. No dash is added if it was a beginning of the name: `MyProperty` -> `my-property`. Also, no dash is added if it was already there:
115          * `camel-Case-WithDashes` -> `camel-case-with-dashes`.
116          *
117          * **Acronyms**
118          *
119          * Since acronym rules are quite complex, it is recommended to lowercase all acronyms in source code.
120          * If there is an uppercase acronym — a sequence of uppercase chars — they are considered as a whole word from the start to second-to-last character of the sequence:
121          * `URLMapping` -> `url-mapping`, `myHTTPAuth` -> `my-http-auth`. Non-letter characters allow the word to continue:
122          * `myHTTP2APIKey` -> `my-http2-api-key`,  `myHTTP2fastApiKey` -> `my-http2fast-api-key`.
123          *
124          * **Note on cases**
125          *
126          * Whether a character is in upper case is determined by the result of [Char.isUpperCase] function.
127          * Lowercase transformation is performed by [Char.lowercaseChar], not by [Char.lowercase],
128          * and therefore does not support one-to-many and many-to-one character mappings.
129          * See the documentation of these functions for details.
130          */
131         @ExperimentalSerializationApi
132         public val KebabCase: JsonNamingStrategy = object : JsonNamingStrategy {
133             override fun serialNameForJson(
134                 descriptor: SerialDescriptor,
135                 elementIndex: Int,
136                 serialName: String
137             ): String = convertCamelCase(serialName, '-')
138 
139             override fun toString(): String = "kotlinx.serialization.json.JsonNamingStrategy.KebabCase"
140         }
141 
142         private fun convertCamelCase(
143             serialName: String,
144             delimiter: Char
145         ) = buildString(serialName.length * 2) {
146                 var bufferedChar: Char? = null
147                 var previousUpperCharsCount = 0
148 
149                 serialName.forEach { c ->
150                     if (c.isUpperCase()) {
151                         if (previousUpperCharsCount == 0 && isNotEmpty() && last() != delimiter)
152                             append(delimiter)
153 
154                         bufferedChar?.let(::append)
155 
156                         previousUpperCharsCount++
157                         bufferedChar = c.lowercaseChar()
158                     } else {
159                         if (bufferedChar != null) {
160                             if (previousUpperCharsCount > 1 && c.isLetter()) {
161                                 append(delimiter)
162                             }
163                             append(bufferedChar)
164                             previousUpperCharsCount = 0
165                             bufferedChar = null
166                         }
167                         append(c)
168                     }
169                 }
170 
171                 if (bufferedChar != null) {
172                     append(bufferedChar)
173                 }
174             }
175 
176     }
177 }
178