Skip to content

Commit 4e9c17a

Browse files
authored
feat(json): add config to SchemaIntrospector, implement custom annotation description extraction for SerializationClassJsonSchemaGenerator (#196)
## Description Added generic `config` field to `SchemaIntrospector` interface to allow customizing schema introspection. Added `DescriptionExtractor` functional interface to allow customizing property/type description extraction logic. Added config for `SerializationClassSchemaIntrospector` to allow processing custom description annotations **Related Issues:** closes #193
1 parent e65301c commit 4e9c17a

File tree

17 files changed

+92
-60
lines changed

17 files changed

+92
-60
lines changed

kotlinx-schema-generator-core/src/commonMain/kotlin/kotlinx/schema/generator/core/AbstractSchemaGenerator.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import kotlinx.schema.generator.core.ir.TypeGraphTransformer
88
*
99
* @param T the type of the object for which the schema is being generated
1010
* @param R the type of the resulting schema representation
11+
* @param C the type of the configuration used by the introspector
1112
* @property introspector the component responsible for introspecting the input and producing a type graph
1213
* @property typeGraphTransformer the component responsible for converting the type graph
1314
* into the desired schema representation
1415
*/
15-
public abstract class AbstractSchemaGenerator<T : Any, R : Any>(
16-
protected val introspector: SchemaIntrospector<T>,
16+
public abstract class AbstractSchemaGenerator<T : Any, R : Any, C : Any>(
17+
protected val introspector: SchemaIntrospector<T, C>,
1718
protected val typeGraphTransformer: TypeGraphTransformer<R, *>,
1819
) : SchemaGenerator<T, R> {
1920
protected abstract fun getRootName(target: T): String

kotlinx-schema-generator-core/src/commonMain/kotlin/kotlinx/schema/generator/core/ir/Introspections.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,18 @@ public object Introspections {
114114
null
115115
}
116116
}
117+
118+
/**
119+
* Functional interface describing a strategy for extracting a property/type description from a list of annotations
120+
* associated with it.
121+
* It's used to allow custom description annotations.
122+
*/
123+
public fun interface DescriptionExtractor {
124+
/**
125+
* Extracts a description from a list of annotations.
126+
*
127+
* @param annotations List of annotations to inspect for a description
128+
* @return The description text if found, or null if no description is present
129+
*/
130+
public fun extract(annotations: List<Annotation>): String?
131+
}

kotlinx-schema-generator-core/src/commonMain/kotlin/kotlinx/schema/generator/core/ir/SchemaIntrospector.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ package kotlinx.schema.generator.core.ir
1212
* 5. Introspectors instantiate context and call `toRef()` / `convertToTypeRef()`
1313
* 6. Return TypeGraph(root, context.nodes)
1414
*/
15-
public interface SchemaIntrospector<T> {
15+
public interface SchemaIntrospector<T, C> {
16+
/**
17+
* Configuration object for the introspector
18+
*/
19+
public val config: C
20+
1621
public fun introspect(root: T): TypeGraph
1722
}

kotlinx-schema-generator-core/src/jvmMain/kotlin/kotlinx/schema/generator/reflect/ReflectionClassIntrospector.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ import kotlin.reflect.KProperty
2929
* - Requires classes to have a primary constructor
3030
* - Type parameters are not fully supported
3131
*/
32-
public object ReflectionClassIntrospector : SchemaIntrospector<KClass<*>> {
32+
public object ReflectionClassIntrospector : SchemaIntrospector<KClass<*>, Unit> {
33+
override val config: Unit = Unit
34+
3335
override fun introspect(root: KClass<*>): TypeGraph {
3436
val context = IntrospectionContext()
3537
val rootRef = context.convertToTypeRef(root)

kotlinx-schema-generator-core/src/jvmMain/kotlin/kotlinx/schema/generator/reflect/ReflectionFunctionIntrospector.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ import kotlin.reflect.KParameter
2222
* val typeGraph = ReflectionFunctionIntrospector.introspect(::myFunction)
2323
* ```
2424
*/
25-
public object ReflectionFunctionIntrospector : SchemaIntrospector<KCallable<*>> {
25+
public object ReflectionFunctionIntrospector : SchemaIntrospector<KCallable<*>, Unit> {
26+
override val config: Unit = Unit
27+
2628
override fun introspect(root: KCallable<*>): TypeGraph {
2729
require(!root.isSuspend) { "Suspend functions are not supported" }
2830
require(root.parameters.none { it.kind == KParameter.Kind.EXTENSION_RECEIVER }) {

kotlinx-schema-generator-core/src/jvmTest/kotlin/kotlinx/schema/generator/core/AbstractSchemaGeneratorTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,22 @@ import kotlin.reflect.KClass
1515
@ExtendWith(MockKExtension::class)
1616
class AbstractSchemaGeneratorTest {
1717
@MockK
18-
private lateinit var introspector: SchemaIntrospector<KClass<*>>
18+
private lateinit var introspector: SchemaIntrospector<KClass<*>, Unit>
1919

2020
@MockK
2121
private lateinit var emitter: TypeGraphTransformer<Map<String, String>, *>
2222

2323
@MockK
2424
private lateinit var typeGraph: TypeGraph
2525

26-
private lateinit var generator: AbstractSchemaGenerator<KClass<*>, Map<String, String>>
26+
private lateinit var generator: AbstractSchemaGenerator<KClass<*>, Map<String, String>, Unit>
2727

2828
private val rootName = AbstractSchemaGeneratorTest::class.qualifiedName!!
2929

3030
@BeforeEach
3131
fun setUp() {
3232
generator =
33-
object : AbstractSchemaGenerator<KClass<*>, Map<String, String>>(introspector, emitter) {
33+
object : AbstractSchemaGenerator<KClass<*>, Map<String, String>, Unit>(introspector, emitter) {
3434
override fun getRootName(target: KClass<*>): String = requireNotNull(target.qualifiedName)
3535

3636
override fun targetType(): KClass<KClass<*>> = KClass::class

kotlinx-schema-generator-json/src/commonMain/kotlin/kotlinx/schema/generator/json/serialization/SerializationClassJsonSchemaGenerator.kt

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,28 @@ import kotlin.reflect.KClass
1616
* with a configurable `JsonSchemaConfig` to define schema generation behavior.
1717
*
1818
* @constructor Creates an instance of `SerializationClassJsonSchemaGenerator`.
19-
* @param config Configuration for generating JSON Schemas, such as formatting details
19+
* @param json The [Json] instance used for serializing schema objects.
20+
* @param introspectorConfig Configuration for introspecting serial descriptors.
21+
* @param jsonSchemaConfig Configuration for generating JSON Schemas, such as formatting details
2022
* and handling of optional nullable properties. Defaults to [JsonSchemaConfig.Default].
2123
*/
2224
public class SerializationClassJsonSchemaGenerator(
23-
private val json: Json,
24-
config: JsonSchemaConfig,
25-
) : AbstractSchemaGenerator<SerialDescriptor, JsonSchema>(
26-
introspector = SerializationClassSchemaIntrospector(json),
25+
private val json: Json =
26+
Json {
27+
encodeDefaults = false
28+
classDiscriminator = "type"
29+
classDiscriminatorMode = kotlinx.serialization.json.ClassDiscriminatorMode.ALL_JSON_OBJECTS
30+
},
31+
introspectorConfig: SerializationClassSchemaIntrospector.Config = SerializationClassSchemaIntrospector.Config(),
32+
jsonSchemaConfig: JsonSchemaConfig = JsonSchemaConfig.Default,
33+
) : AbstractSchemaGenerator<SerialDescriptor, JsonSchema, SerializationClassSchemaIntrospector.Config>(
34+
introspector = SerializationClassSchemaIntrospector(introspectorConfig, json),
2735
typeGraphTransformer =
2836
TypeGraphToJsonSchemaTransformer(
29-
config = config,
37+
config = jsonSchemaConfig,
3038
json = json,
3139
),
3240
) {
33-
public constructor() : this(
34-
json = Json {
35-
encodeDefaults = false
36-
classDiscriminator = "type"
37-
classDiscriminatorMode = kotlinx.serialization.json.ClassDiscriminatorMode.ALL_JSON_OBJECTS
38-
},
39-
config = JsonSchemaConfig.Default,
40-
)
41-
4241
override fun getRootName(target: SerialDescriptor): String = target.serialName
4342

4443
override fun targetType(): KClass<SerialDescriptor> = SerialDescriptor::class
@@ -56,14 +55,6 @@ public class SerializationClassJsonSchemaGenerator(
5655
* kotlinx.serialization descriptors. It simplifies the creation of schemas
5756
* without requiring explicit configuration.
5857
*/
59-
public val Default: SerializationClassJsonSchemaGenerator =
60-
SerializationClassJsonSchemaGenerator(
61-
json = Json {
62-
encodeDefaults = false
63-
classDiscriminator = "type"
64-
classDiscriminatorMode = kotlinx.serialization.json.ClassDiscriminatorMode.ALL_JSON_OBJECTS
65-
},
66-
config = JsonSchemaConfig.Default,
67-
)
58+
public val Default: SerializationClassJsonSchemaGenerator = SerializationClassJsonSchemaGenerator()
6859
}
6960
}

kotlinx-schema-generator-json/src/commonMain/kotlin/kotlinx/schema/generator/json/serialization/SerializationClassSchemaIntrospector.kt

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package kotlinx.schema.generator.json.serialization
22

3+
import kotlinx.schema.generator.core.ir.DescriptionExtractor
34
import kotlinx.schema.generator.core.ir.SchemaIntrospector
45
import kotlinx.schema.generator.core.ir.TypeGraph
56
import kotlinx.serialization.descriptors.SerialDescriptor
@@ -15,20 +16,26 @@ import kotlinx.serialization.json.Json
1516
* Defaults to a Json instance with encodeDefaults = false.
1617
*/
1718
public class SerializationClassSchemaIntrospector(
18-
private val json: Json = Json {
19-
encodeDefaults = false
20-
classDiscriminator = "type"
21-
classDiscriminatorMode = kotlinx.serialization.json.ClassDiscriminatorMode.ALL_JSON_OBJECTS
22-
},
23-
) : SchemaIntrospector<SerialDescriptor> {
19+
override val config: Config = Config(),
20+
private val json: Json =
21+
Json {
22+
encodeDefaults = false
23+
classDiscriminator = "type"
24+
classDiscriminatorMode = kotlinx.serialization.json.ClassDiscriminatorMode.ALL_JSON_OBJECTS
25+
},
26+
) : SchemaIntrospector<SerialDescriptor, SerializationClassSchemaIntrospector.Config> {
27+
public data class Config(
28+
val descriptionExtractor: DescriptionExtractor = DescriptionExtractor { null },
29+
)
30+
2431
/**
2532
* Introspects a serial descriptor into a [TypeGraph].
2633
*
2734
* @param root The root serial descriptor to introspect
2835
* @return A TypeGraph containing the root type reference and all discovered type nodes
2936
*/
3037
public override fun introspect(root: SerialDescriptor): TypeGraph {
31-
val context = SerializationIntrospectionContext(json)
38+
val context = SerializationIntrospectionContext(json, config)
3239
val rootRef = context.toRef(root)
3340
return TypeGraph(root = rootRef, nodes = context.nodes())
3441
}

kotlinx-schema-generator-json/src/commonMain/kotlin/kotlinx/schema/generator/json/serialization/SerializationIntrospectionContext.kt

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import kotlinx.serialization.descriptors.PrimitiveKind as SerialPrimitiveKind
3636
@OptIn(InternalSchemaGeneratorApi::class)
3737
internal class SerializationIntrospectionContext(
3838
private val json: Json,
39+
private val config: SerializationClassSchemaIntrospector.Config,
3940
) : BaseIntrospectionContext<SerialDescriptor, SerialDescriptor>() {
4041
/**
4142
* Returns the discovered type nodes.
@@ -356,26 +357,18 @@ internal class SerializationIntrospectionContext(
356357
private fun descriptorId(descriptor: SerialDescriptor): TypeId = TypeId(descriptor.serialName)
357358

358359
/**
359-
* Extracts the @Description annotation value from a descriptor's annotations.
360+
* Extracts description from a list of type annotations.
360361
*/
361362
private fun extractDescription(descriptor: SerialDescriptor): String? =
362-
descriptor.annotations
363-
.filterIsInstance<Description>()
364-
.firstOrNull()
365-
?.value
363+
config.descriptionExtractor.extract(descriptor.annotations)
366364

367365
/**
368-
* Extracts the @Description annotation value from a descriptor element's annotations.
366+
* Extracts description from a list of element annotations.
369367
*/
370368
private fun extractElementDescription(
371369
descriptor: SerialDescriptor,
372370
index: Int,
373-
): String? =
374-
descriptor
375-
.getElementAnnotations(index)
376-
.filterIsInstance<Description>()
377-
.firstOrNull()
378-
?.value
371+
): String? = config.descriptionExtractor.extract(descriptor.getElementAnnotations(index))
379372

380373
/**
381374
* Returns a new [TypeRef] with the specified nullable flag.

kotlinx-schema-generator-json/src/commonTest/kotlin/kotlinx/schema/generator/json/serialization/SerializationClassJsonSchemaGeneratorTest.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@ package kotlinx.schema.generator.json.serialization
22

33
import io.kotest.assertions.json.shouldEqualJson
44
import kotlinx.schema.json.encodeToString
5+
import kotlinx.serialization.SerialInfo
56
import kotlinx.serialization.Serializable
67
import kotlinx.serialization.json.Json
78
import kotlin.test.Test
89

910
class SerializationClassJsonSchemaGeneratorTest {
11+
@SerialInfo
12+
annotation class CustomDescription(val value: String)
13+
1014
@Serializable
1115
data class Person(
16+
@property:CustomDescription("First name")
1217
val firstName: String,
1318
)
1419

@@ -22,14 +27,21 @@ class SerializationClassJsonSchemaGeneratorTest {
2227
"type": "object",
2328
"properties": {
2429
"firstName": {
25-
"type": "string"
30+
"type": "string",
31+
"description": "First name"
2632
}
2733
},
2834
"additionalProperties": false
2935
}
3036
"""
3137

32-
val generator = SerializationClassJsonSchemaGenerator()
38+
val generator = SerializationClassJsonSchemaGenerator(
39+
introspectorConfig = SerializationClassSchemaIntrospector.Config(
40+
descriptionExtractor = { annotations ->
41+
annotations.filterIsInstance<CustomDescription>().firstOrNull()?.value
42+
},
43+
),
44+
)
3345

3446
@Test
3547
fun `Should generate JsonSchema from SerialDescriptor`() {

0 commit comments

Comments
 (0)