Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,10 @@ class ModelGenerator(private val modelPackage: String) {
}

schema.isPrimitiveOnly -> {
listOf(generateTypeAlias(schema, STRING))
val targetType = schema.underlyingType
?.let { TypeMapping.toTypeName(it, modelPackage) }
?: STRING
listOf(generateTypeAlias(schema, targetType))
}

else -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ data class SchemaModel(
val oneOf: List<TypeRef>?,
val anyOf: List<TypeRef>?,
val discriminator: Discriminator?,
val underlyingType: TypeRef? = null,
) {
val isNested get() = name.contains(".")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ object SpecParser {
Discriminator(propertyName = propertyName, mapping = disc.mapping.orEmpty())
}

// Resolve underlying type for primitive-only / $ref-wrapper schemas.
// Uses $ref for wrapper schemas, otherwise resolves structurally
// from type/format to bypass componentSchemaIdentity (which would self-reference).
val underlyingType = schema
.takeIf { properties.isEmpty() && allOf.isNullOrEmpty() && oneOf.isNullOrEmpty() && anyOf.isNullOrEmpty() }
?.let { s -> s.`$ref`?.removePrefix(SCHEMA_PREFIX)?.let(TypeRef::Reference) ?: s.resolveByType() }
?.takeUnless { it is TypeRef.Unknown }

return SchemaModel(
name = name,
description = schema.description,
Expand All @@ -230,6 +238,7 @@ object SpecParser {
oneOf = oneOf?.let { it.map(TypeRef::Reference).ifEmpty { null } },
anyOf = anyOf?.let { it.map(TypeRef::Reference).ifEmpty { null } },
discriminator = discriminator,
underlyingType = underlyingType,
)
}

Expand Down Expand Up @@ -313,26 +322,30 @@ object SpecParser {
private fun Schema<*>.toTypeRef(contextName: String? = null): TypeRef = contextName?.let { toInlineTypeRef(it) }
?: (resolveName() ?: allOf?.singleOrNull()?.resolveName())?.let(TypeRef::Reference)
?: TypeRef.Unknown.takeIf { (allOf?.size ?: 0) > 1 }
?: when (type) {
"string" -> STRING_FORMAT_MAP[format] ?: TypeRef.Primitive(PrimitiveType.STRING)
?: resolveByType(contextName)

"integer" -> INTEGER_FORMAT_MAP[format] ?: TypeRef.Primitive(PrimitiveType.INT)
/** Resolves a [TypeRef] based on the schema's structural type/format, ignoring component identity. */
context(_: ComponentSchemaIdentity, _: ComponentSchemas)
private fun Schema<*>.resolveByType(contextName: String? = null): TypeRef = when (type) {
"string" -> STRING_FORMAT_MAP[format] ?: TypeRef.Primitive(PrimitiveType.STRING)

"number" -> NUMBER_FORMAT_MAP[format] ?: TypeRef.Primitive(PrimitiveType.DOUBLE)
"integer" -> INTEGER_FORMAT_MAP[format] ?: TypeRef.Primitive(PrimitiveType.INT)

"boolean" -> TypeRef.Primitive(PrimitiveType.BOOLEAN)
"number" -> NUMBER_FORMAT_MAP[format] ?: TypeRef.Primitive(PrimitiveType.DOUBLE)

"array" -> TypeRef.Array(items?.toTypeRef(contextName?.let { "${it}Item" }) ?: TypeRef.Unknown)
"boolean" -> TypeRef.Primitive(PrimitiveType.BOOLEAN)

"object" -> when (val ap = additionalProperties) {
is Schema<*> -> TypeRef.Map(ap.toTypeRef())
is Boolean -> if (ap) TypeRef.Map(TypeRef.Unknown) else TypeRef.Unknown
else -> title?.let(TypeRef::Reference) ?: TypeRef.Unknown
}
"array" -> TypeRef.Array(items?.toTypeRef(contextName?.let { "${it}Item" }) ?: TypeRef.Unknown)

else -> TypeRef.Unknown
"object" -> when (val ap = additionalProperties) {
is Schema<*> -> TypeRef.Map(ap.toTypeRef())
is Boolean -> if (ap) TypeRef.Map(TypeRef.Unknown) else TypeRef.Unknown
else -> title?.let(TypeRef::Reference) ?: TypeRef.Unknown
}

else -> TypeRef.Unknown
}

context(_: ComponentSchemaIdentity, _: ComponentSchemas)
private fun Schema<*>.toInlineTypeRef(contextName: String): TypeRef? = takeIf { isInlineObject }?.let {
val required = required.orEmpty().toSet()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,7 @@ class ModelGeneratorTest {
// -- Primitive-only type alias tests --

@Test
fun `primitive only schema generates type alias`() {
fun `primitive only schema generates type alias to String by default`() {
val groupIdSchema = SchemaModel(
name = "GroupId",
description = null,
Expand All @@ -742,6 +742,7 @@ class ModelGeneratorTest {
oneOf = null,
anyOf = null,
discriminator = null,
underlyingType = null,
)
val files = generator.generate(spec(schemas = listOf(groupIdSchema)))
assertEquals(1, files.size)
Expand All @@ -755,6 +756,139 @@ class ModelGeneratorTest {

val typeAlias = typeAliases.first()
assertEquals("GroupId", typeAlias.name)
assertEquals("kotlin.String", typeAlias.type.toString(), "Default typealias should be String")
}

@Test
fun `primitive only schema with integer underlyingType generates typealias to Int`() {
val schema = SchemaModel(
name = "IntId",
description = null,
properties = emptyList(),
requiredProperties = emptySet(),
allOf = null,
oneOf = null,
anyOf = null,
discriminator = null,
underlyingType = TypeRef.Primitive(PrimitiveType.INT),
)
val files = generator.generate(spec(schemas = listOf(schema)))
val typeAlias = files
.first()
.members
.filterIsInstance<com.squareup.kotlinpoet.TypeAliasSpec>()
.first()
assertEquals("kotlin.Int", typeAlias.type.toString())
}

@Test
fun `primitive only schema with boolean underlyingType generates typealias to Boolean`() {
val schema = SchemaModel(
name = "Flag",
description = null,
properties = emptyList(),
requiredProperties = emptySet(),
allOf = null,
oneOf = null,
anyOf = null,
discriminator = null,
underlyingType = TypeRef.Primitive(PrimitiveType.BOOLEAN),
)
val files = generator.generate(spec(schemas = listOf(schema)))
val typeAlias = files
.first()
.members
.filterIsInstance<com.squareup.kotlinpoet.TypeAliasSpec>()
.first()
assertEquals("kotlin.Boolean", typeAlias.type.toString())
}

@Test
fun `primitive only schema with long underlyingType generates typealias to Long`() {
val schema = SchemaModel(
name = "BigId",
description = null,
properties = emptyList(),
requiredProperties = emptySet(),
allOf = null,
oneOf = null,
anyOf = null,
discriminator = null,
underlyingType = TypeRef.Primitive(PrimitiveType.LONG),
)
val files = generator.generate(spec(schemas = listOf(schema)))
val typeAlias = files
.first()
.members
.filterIsInstance<com.squareup.kotlinpoet.TypeAliasSpec>()
.first()
assertEquals("kotlin.Long", typeAlias.type.toString())
}

@Test
fun `primitive only schema with double underlyingType generates typealias to Double`() {
val schema = SchemaModel(
name = "Score",
description = null,
properties = emptyList(),
requiredProperties = emptySet(),
allOf = null,
oneOf = null,
anyOf = null,
discriminator = null,
underlyingType = TypeRef.Primitive(PrimitiveType.DOUBLE),
)
val files = generator.generate(spec(schemas = listOf(schema)))
val typeAlias = files
.first()
.members
.filterIsInstance<com.squareup.kotlinpoet.TypeAliasSpec>()
.first()
assertEquals("kotlin.Double", typeAlias.type.toString())
}

@Test
fun `primitive only schema with array underlyingType generates typealias to List`() {
val schema = SchemaModel(
name = "Tags",
description = null,
properties = emptyList(),
requiredProperties = emptySet(),
allOf = null,
oneOf = null,
anyOf = null,
discriminator = null,
underlyingType = TypeRef.Array(TypeRef.Primitive(PrimitiveType.STRING)),
)
val files = generator.generate(spec(schemas = listOf(schema)))
val typeAlias = files
.first()
.members
.filterIsInstance<com.squareup.kotlinpoet.TypeAliasSpec>()
.first()
assertEquals("kotlin.collections.List<kotlin.String>", typeAlias.type.toString())
}

@Test
fun `primitive only schema with reference underlyingType generates typealias to referenced type`() {
val schema = SchemaModel(
name = "Wrapper",
description = null,
properties = emptyList(),
requiredProperties = emptySet(),
allOf = null,
oneOf = null,
anyOf = null,
discriminator = null,
underlyingType = TypeRef.Reference("OtherSchema"),
)
val files = generator.generate(spec(schemas = listOf(schema)))
val typeAlias = files
.first()
.members
.filterIsInstance<com.squareup.kotlinpoet.TypeAliasSpec>()
.first()
assertEquals("com.example.model.OtherSchema", typeAlias.type.toString())
}

@Test
Expand Down
Loading
Loading