Repository: cjbooms/fabrikt
Pull request #13 (2020-08-04 11:57:31)

Issue #12: Add Java Serialization Option to Models
Add Java Serialization Option to Models


Patch:
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
deleted file mode 100644
index 478d7d7..0000000
--- a/.github/workflows/publish.yml
+++ /dev/null
@@ -1,15 +0,0 @@
-name: Publish package to the Maven Central Repository
-# The job will be triggered when a release is created
-on:
-  release:
-    types: [created]
-jobs:
-  publish:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v2
-      - name: Publish to the Maven Central Repository
-        run: ./gradlew clean -PsigningKey=${{ secrets.GPG_SIGNING_KEY }} -PsigningPassword=${{ secrets.GPG_SIGNING_PASSWORD }} publish
-        env:
-          OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
-          OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
diff --git a/README.md b/README.md
index 3e4be01..de7723d 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,9 @@ This section documents the available CLI parameters for controlling what gets ge
  |   `--http-client-opts` | Select the options for the http client code that you want to be generated.
  |                        | CHOOSE ANY OF:
  |                        |   `RESILIENCE4J` - Generates a fault tolerance service for the client using the following library "io.github.resilience4j:resilience4j-all:1.2.0"
+ |   `--http-model-opts`  | Select the options for the http models that you want to be generated.
+ |                        | CHOOSE ANY OF:
+ |                        |   `JAVA_SERIALIZATION` - This option adds Java Serializable interface to the generated models
  |   `--output-directory` | Allows the generation dir to be overridden. Defaults to current dir
  |   `--targets`          | Targets are the parts of the application that you want to be generated.
  |                        | CHOOSE ANY OF:
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt
index 5a0cd2d..00b23bc 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGen.kt
@@ -24,6 +24,7 @@ object CodeGen {
             codeGenArgs.outputDirectory,
             codeGenArgs.targets,
             codeGenArgs.apiFragments.map { it.toFile().readText() },
+            codeGenArgs.modelOptions,
             codeGenArgs.clientOptions
         )
     }
@@ -34,6 +35,7 @@ object CodeGen {
         outputDir: Path,
         codeGenTypes: Set<CodeGenerationType>,
         apiFragments: List<String> = emptyList(),
+        modelOptions: Set<ModelCodeGenOptionType>,
         clientOptions: Set<ClientCodeGenOptionType>
     ) {
         val suppliedApi = pathToApi.toFile().readText()
@@ -43,7 +45,7 @@ object CodeGen {
 
         val packages = Packages(basePackage)
         val sourceApi = SourceApi.create(suppliedApi, apiFragments, baseDir)
-        val generator = CodeGenerator(packages, sourceApi, codeGenTypes, clientOptions)
+        val generator = CodeGenerator(packages, sourceApi, codeGenTypes, modelOptions, clientOptions)
 
         generator.generate().forEach { it.writeFileTo(outputDir.toFile()) }
     }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenArgs.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenArgs.kt
index e230425..12739dc 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenArgs.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenArgs.kt
@@ -76,6 +76,13 @@ class CodeGenArgs {
     )
     var targets: Set<CodeGenerationType> = emptySet()
 
+    @Parameter(
+        names = ["--http-model-opts"],
+        description = "Select the options for the http models that you want to be generated.",
+        converter = ModelCodeGenOptionConverter::class
+    )
+    var modelOptions: Set<ModelCodeGenOptionType> = emptySet()
+
     @Parameter(
         names = ["--http-client-opts"],
         description = "Select the options for the http client code that you want to be generated.",
@@ -89,6 +96,10 @@ class CodeGenerationTypesConverter : IStringConverter<CodeGenerationType> {
         convertToEnumValue(value)
 }
 
+class ModelCodeGenOptionConverter : IStringConverter<ModelCodeGenOptionType> {
+    override fun convert(value: String): ModelCodeGenOptionType = convertToEnumValue(value)
+}
+
 class ClientCodeGenOptionConverter : IStringConverter<ClientCodeGenOptionType> {
     override fun convert(value: String): ClientCodeGenOptionType =
         convertToEnumValue(value)
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
index 335a34c..d338159 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
@@ -38,6 +38,12 @@ enum class ClientCodeGenOptionType(private val description: String) {
     override fun toString() = "`${super.toString()}` - $description"
 }
 
+enum class ModelCodeGenOptionType(val description: String) {
+    JAVA_SERIALIZATION("This option adds Java Serializable interface to the generated models");
+
+    override fun toString() = "`${super.toString()}` - $description"
+}
+
 data class CodeGenOptions(
     val clientOptions: Set<ClientCodeGenOptionType> = emptySet()
 )
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt
index 0d5f2a3..51a6366 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenerator.kt
@@ -21,6 +21,7 @@ class CodeGenerator(
     private val packages: Packages,
     private val sourceApi: SourceApi,
     private val generationTypes: Set<CodeGenerationType>,
+    private val modelOptions: Set<ModelCodeGenOptionType>,
     private val codeGenOptions: Set<ClientCodeGenOptionType>
 ) {
 
@@ -49,7 +50,7 @@ class CodeGenerator(
     private fun sourceSet(fileSpec: Collection<FileSpec>) = setOf(KotlinSourceSet(fileSpec))
 
     private fun models(): Models =
-        JacksonModelGenerator(packages, sourceApi).generate()
+        JacksonModelGenerator(packages, sourceApi, modelOptions).generate()
 
     private fun services(): Services =
         SpringServiceInterfaceGenerator(packages, sourceApi, models().models).generate()
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
index d940842..4b5e890 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
@@ -1,5 +1,6 @@
 package com.cjbooms.fabrikt.generators.model
 
+import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType
 import com.cjbooms.fabrikt.configurations.Packages
 import com.cjbooms.fabrikt.generators.ClassType
 import com.cjbooms.fabrikt.generators.PropertyUtils.addToClass
@@ -37,8 +38,13 @@ import com.squareup.kotlinpoet.PropertySpec
 import com.squareup.kotlinpoet.TypeName
 import com.squareup.kotlinpoet.TypeSpec
 import com.squareup.kotlinpoet.asTypeName
+import java.io.Serializable
 
-class JacksonModelGenerator(private val packages: Packages, private val sourceApi: SourceApi) {
+class JacksonModelGenerator(
+    private val packages: Packages,
+    private val sourceApi: SourceApi,
+    private val options: Set<ModelCodeGenOptionType> = emptySet()
+) {
     companion object {
         fun toModelType(basePackage: String, typeInfo: KotlinTypeInfo, isRequired: Boolean = true): TypeName {
             val className =
@@ -215,7 +221,8 @@ class JacksonModelGenerator(private val packages: Packages, private val sourceAp
                         modelName
                     )
                 )
-                .addModifiers(KModifier.DATA),
+                .addModifiers(KModifier.DATA)
+                .addSerializableInterface(),
             ClassType.VANILLA_MODEL
         )
 
@@ -263,6 +270,7 @@ class JacksonModelGenerator(private val packages: Packages, private val sourceAp
                     )
                 )
                 .addModifiers(KModifier.DATA)
+                .addSerializableInterface()
                 .superclass(
                     toModelType(
                         packages.base,
@@ -292,4 +300,15 @@ class JacksonModelGenerator(private val packages: Packages, private val sourceAp
         }
         return classBuilder.primaryConstructor(constructorBuilder.build()).build()
     }
+
+    private fun TypeSpec.Builder.addSerializableInterface(): TypeSpec.Builder =
+        ifJavaSerializableIsEnabled(this) {
+            it.addSuperinterface(Serializable::class)
+        }
+
+    private fun ifJavaSerializableIsEnabled(builder: TypeSpec.Builder, block: (TypeSpec.Builder) -> TypeSpec.Builder): TypeSpec.Builder =
+        if (options.any { it == ModelCodeGenOptionType.JAVA_SERIALIZATION })
+            block(builder)
+        else
+            builder
 }


Test patch:
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
index be6ab13..54304bd 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
@@ -1,5 +1,6 @@
 package com.cjbooms.fabrikt.generators
 
+import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType
 import com.cjbooms.fabrikt.configurations.Packages
 import com.cjbooms.fabrikt.generators.model.JacksonModelGenerator
 import com.cjbooms.fabrikt.model.Models
@@ -81,6 +82,19 @@ class ModelGeneratorTest {
         assertThat(Linter.lintString(models.files.first().toString())).isEqualTo(expectedModels)
     }
 
+    @Test
+    fun `serializable models are generated from a full API definition when the java-serialized option is set`() {
+        val basePackage = "examples.javaSerializableModels"
+        val spec = javaClass.getResource("/examples/javaSerializableModels/open-api/api.yaml").readText()
+        val expectedModels = javaClass.getResource("/examples/javaSerializableModels/models/Models.kt").readText()
+
+        val models = JacksonModelGenerator(Packages(basePackage), SourceApi(spec), setOf(ModelCodeGenOptionType.JAVA_SERIALIZATION))
+            .generate()
+            .toSingleFile()
+
+        assertThat(models).isEqualTo(expectedModels)
+    }
+
     private fun Models.toSingleFile(): String {
         val destPackage = if (models.isNotEmpty()) models.first().destinationPackage else ""
         val singleFileBuilder = FileSpec.builder(destPackage, "dummyFilename")
diff --git a/src/test/resources/examples/javaSerializableModels/models/Models.kt b/src/test/resources/examples/javaSerializableModels/models/Models.kt
new file mode 100644
index 0000000..6b7cf9c
--- /dev/null
+++ b/src/test/resources/examples/javaSerializableModels/models/Models.kt
@@ -0,0 +1,154 @@
+package examples.javaSerializableModels.models
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import com.fasterxml.jackson.annotation.JsonValue
+import java.io.Serializable
+import java.time.OffsetDateTime
+import javax.validation.Valid
+import javax.validation.constraints.NotNull
+import javax.validation.constraints.Size
+import kotlin.Boolean
+import kotlin.Int
+import kotlin.String
+import kotlin.collections.List
+
+data class QueryResult(
+    @param:JsonProperty("items")
+    @get:JsonProperty("items")
+    @get:NotNull
+    @get:Size(min = 0)
+    @get:Valid
+    val items: List<Content>
+) : Serializable
+
+@JsonTypeInfo(
+    use = JsonTypeInfo.Id.NAME,
+    include = JsonTypeInfo.As.EXISTING_PROPERTY,
+    property = "model_type",
+    visible = true
+)
+@JsonSubTypes(
+    JsonSubTypes.Type(
+        value = FirstModel::class,
+        name =
+            "first_model"
+    ),
+    JsonSubTypes.Type(
+        value = SecondModel::class,
+        name =
+            "second_model"
+    ),
+    JsonSubTypes.Type(value = ThirdModel::class, name = "third_model")
+)
+sealed class Content(
+    open val id: String? = null,
+    open val firstAttr: OffsetDateTime? = null,
+    open val secondAttr: String? = null,
+    open val thirdAttr: ThirdAttr? = null,
+    open val etag: String? = null
+) {
+    abstract val modelType: ModelType
+}
+
+enum class ThirdAttr(
+    @JsonValue
+    val value: String
+) {
+    ENUM_TYPE_1("enum_type_1"),
+
+    ENUM_TYPE_2("enum_type_2");
+}
+
+enum class ModelType(
+    @JsonValue
+    val value: String
+) {
+    FIRST_MODEL("first_model"),
+
+    SECOND_MODEL("second_model"),
+
+    THIRD_MODEL("third_model");
+}
+
+data class FirstModel(
+    @param:JsonProperty("id")
+    @get:JsonProperty("id")
+    override val id: String? = null,
+    @param:JsonProperty("first_attr")
+    @get:JsonProperty("first_attr")
+    override val firstAttr: OffsetDateTime? = null,
+    @param:JsonProperty("second_attr")
+    @get:JsonProperty("second_attr")
+    override val secondAttr: String? = null,
+    @param:JsonProperty("third_attr")
+    @get:JsonProperty("third_attr")
+    override val thirdAttr: ThirdAttr? = null,
+    @param:JsonProperty("etag")
+    @get:JsonProperty("etag")
+    override val etag: String? = null,
+    @param:JsonProperty("extra_first_attr")
+    @get:JsonProperty("extra_first_attr")
+    val extraFirstAttr: List<String>? = null
+) : Content(id, firstAttr, secondAttr, thirdAttr, etag), Serializable {
+    @get:JsonProperty("model_type")
+    @get:NotNull
+    override val modelType: ModelType = ModelType.FIRST_MODEL
+}
+
+data class SecondModel(
+    @param:JsonProperty("id")
+    @get:JsonProperty("id")
+    override val id: String? = null,
+    @param:JsonProperty("first_attr")
+    @get:JsonProperty("first_attr")
+    override val firstAttr: OffsetDateTime? = null,
+    @param:JsonProperty("second_attr")
+    @get:JsonProperty("second_attr")
+    override val secondAttr: String? = null,
+    @param:JsonProperty("third_attr")
+    @get:JsonProperty("third_attr")
+    override val thirdAttr: ThirdAttr? = null,
+    @param:JsonProperty("etag")
+    @get:JsonProperty("etag")
+    override val etag: String? = null,
+    @param:JsonProperty("extra_first_attr")
+    @get:JsonProperty("extra_first_attr")
+    val extraFirstAttr: String? = null,
+    @param:JsonProperty("extra_second_attr")
+    @get:JsonProperty("extra_second_attr")
+    val extraSecondAttr: Boolean? = null
+) : Content(id, firstAttr, secondAttr, thirdAttr, etag), Serializable {
+    @get:JsonProperty("model_type")
+    @get:NotNull
+    override val modelType: ModelType = ModelType.SECOND_MODEL
+}
+
+data class ThirdModel(
+    @param:JsonProperty("id")
+    @get:JsonProperty("id")
+    override val id: String? = null,
+    @param:JsonProperty("first_attr")
+    @get:JsonProperty("first_attr")
+    override val firstAttr: OffsetDateTime? = null,
+    @param:JsonProperty("second_attr")
+    @get:JsonProperty("second_attr")
+    override val secondAttr: String? = null,
+    @param:JsonProperty("third_attr")
+    @get:JsonProperty("third_attr")
+    override val thirdAttr: ThirdAttr? = null,
+    @param:JsonProperty("etag")
+    @get:JsonProperty("etag")
+    override val etag: String? = null,
+    @param:JsonProperty("extra_first_attr")
+    @get:JsonProperty("extra_first_attr")
+    val extraFirstAttr: OffsetDateTime? = null,
+    @param:JsonProperty("extra_second_attr")
+    @get:JsonProperty("extra_second_attr")
+    val extraSecondAttr: Int? = null
+) : Content(id, firstAttr, secondAttr, thirdAttr, etag), Serializable {
+    @get:JsonProperty("model_type")
+    @get:NotNull
+    override val modelType: ModelType = ModelType.THIRD_MODEL
+}
diff --git a/src/test/resources/examples/javaSerializableModels/open-api/api.yaml b/src/test/resources/examples/javaSerializableModels/open-api/api.yaml
new file mode 100644
index 0000000..2fa8b07
--- /dev/null
+++ b/src/test/resources/examples/javaSerializableModels/open-api/api.yaml
@@ -0,0 +1,287 @@
+openapi: "3.0.0"
+info:
+  title: "API Example"
+  version: "1.0"
+paths:
+  /example-path-1:
+    get:
+      summary: "GET example path 1"
+      parameters:
+        - $ref: "#/components/parameters/QueryParam1"
+        - $ref: "#/components/parameters/QueryParam2"
+      responses:
+        200:
+          description: "successful operation"
+          headers:
+            Cache-Control:
+              $ref: "#/components/headers/CacheControl"
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/QueryResult"
+
+    post:
+      summary: "POST example path 1"
+      parameters:
+        - $ref: "#/components/parameters/QueryParam1"
+      requestBody:
+        $ref: "#/components/requestBodies/PostBody"
+      responses:
+        201:
+          headers:
+            Location:
+              $ref: "#/components/headers/Location"
+          description: "Successful operation"
+
+  /example-path-2/{path_param}:
+    get:
+      summary: "GET example path 2"
+      parameters:
+        - $ref: "#/components/parameters/PathParam"
+        - $ref: "#/components/parameters/QueryParam2"
+        - $ref: "#/components/parameters/IfNoneMatch"
+      responses:
+        200:
+          description: "successful operation"
+          headers:
+            Cache-Control:
+              $ref: "#/components/headers/CacheControl"
+          content:
+            application/json:
+              schema:
+                discriminator:
+                  propertyName: "model_type"
+                oneOf:
+                  - $ref: "#/components/schemas/FirstModel"
+                  - $ref: "#/components/schemas/SecondModel"
+                  - $ref: "#/components/schemas/ThirdModel"
+        304:
+          description: "Returned in conjunction with use of the If-None-Match header.\
+            \ Indicates that the resource as described by the passed etag value has\
+            \ not changed"
+    head:
+      summary: "HEAD example path 2"
+      parameters:
+        - $ref: "#/components/parameters/PathParam"
+        - $ref: "#/components/parameters/QueryParam3"
+        - $ref: "#/components/parameters/IfNoneMatch"
+      responses:
+        200:
+          description: "Record Exists"
+        304:
+          description: "Returned in conjunction with use of the If-None-Match header.\
+            \ Indicates that the resource as described by the passed etag value has\
+            \ not changed"
+
+    put:
+      summary: "PUT example path 2"
+      parameters:
+        - $ref: "#/components/parameters/PathParam"
+        - $ref: "#/components/parameters/IfMatch"
+      requestBody:
+        $ref: "#/components/requestBodies/PutBody"
+      responses:
+        204:
+          description: "Operation successful"
+
+  /example-path-3/{path_param}/subresource:
+    put:
+      summary: "PUT example path 3"
+      parameters:
+        - $ref: "#/components/parameters/QueryParam1"
+        - $ref: "#/components/parameters/PathParam"
+        - $ref: "#/components/parameters/IfMatch"
+      requestBody:
+        $ref: "#/components/requestBodies/PutBody"
+      responses:
+        204:
+          description: "Operation successful"
+
+components:
+  parameters:
+    PathParam:
+      name: "path_param"
+      description: "The resource id"
+      in: "path"
+      required: true
+      schema:
+        type: "string"
+
+    IfMatch:
+      name: "If-Match"
+      description: "The RFC7232 If-Match header field"
+      in: "header"
+      schema:
+        type: "string"
+      required: true
+
+    IfNoneMatch:
+      name: "If-None-Match"
+      description: "The RFC7232 If-None-Match header field"
+      in: "header"
+      schema:
+        type: "string"
+      required: false
+
+    QueryParam1:
+      name: "query_param1"
+      in: "query"
+      schema:
+        type: "array"
+        items:
+          type: "string"
+      required: false
+      allowEmptyValue: false
+
+    QueryParam2:
+      name: "query_param2"
+      in: "query"
+      schema:
+        type: integer
+      required: false
+
+    QueryParam3:
+      name: "query_param3"
+      in: "query"
+      schema:
+        type: boolean
+      required: false
+
+  headers:
+    Location:
+      description: "The Location header indicates the URL of a newly created resource"
+      schema:
+        type: "string"
+
+    CacheControl:
+      description: "The RFC7234 Cache Control header"
+      schema:
+        type: "string"
+      example: "must-revalidate, max-age=5"
+
+  requestBodies:
+    PostBody:
+      required: true
+      content:
+        application/json:
+          schema:
+            discriminator:
+              propertyName: "model_type"
+            oneOf:
+              - $ref: "#/components/schemas/FirstModel"
+              - $ref: "#/components/schemas/SecondModel"
+              - $ref: "#/components/schemas/ThirdModel"
+    PutBody:
+      required: true
+      content:
+        application/json:
+          schema:
+            $ref: "#/components/schemas/FirstModel"
+
+
+  schemas:
+    QueryResult:
+      type: "object"
+      required:
+        - "items"
+      properties:
+        items:
+          type: "array"
+          minItems: 0
+          items:
+            oneOf:
+              - $ref: "#/components/schemas/FirstModel"
+              - $ref: "#/components/schemas/SecondModel"
+              - $ref: "#/components/schemas/ThirdModel"
+    Content:
+      type: "object"
+      required:
+        - "id"
+        - "attr_1"
+        - "attr_2"
+        - "attr_3"
+        - "etag"
+        - "model_type"
+      properties:
+        id:
+          description: "The unique resource id"
+          type: "string"
+          readOnly: true
+        first_attr:
+          description: "The attribute 1"
+          type: "string"
+          format: "date-time"
+          example: "2016-01-27T10:52:46.406Z"
+          readOnly: true
+        second_attr:
+          description: "The attribute 2"
+          type: "string"
+          readOnly: true
+        third_attr:
+          type: "string"
+          x-extensible-enum:
+            - "enum_type_1"
+            - "enum_type_2"
+          description: "Enum types for attribute 3"
+          example: "enum_type_2"
+        etag:
+          type: "string"
+          description: "Etag value to be used in conjunction with If-Match headers for optimistic locking purposes"
+          readOnly: true
+        model_type:
+          type: "string"
+          description: "The model discrimination type"
+          x-extensible-enum:
+            - "first_model"
+            - "second_model"
+            - "third_model"
+          example: "third_model"
+      discriminator:
+        propertyName: "model_type"
+        mapping:
+          first_model: "#/components/schemas/FirstModel"
+          second_model: "#/components/schemas/SecondModel"
+          third_model: "#/components/schemas/ThirdModel"
+
+    FirstModel:
+      allOf:
+        - $ref: "#/components/schemas/Content"
+        - type: "object"
+          properties:
+            extra_first_attr:
+              description: "The attribute 1 for model 1"
+              type: array
+              items:
+                type: "string"
+                minItems: 1
+                maxItems: 10
+              readOnly: true
+    SecondModel:
+      allOf:
+        - $ref: "#/components/schemas/Content"
+        - type: "object"
+          properties:
+            extra_first_attr:
+              description: "The attribute 1 for model 2"
+              type: "string"
+              readOnly: true
+            extra_second_attr:
+              description: "The attribute 2 for model 2"
+              type: boolean
+              readOnly: true
+
+    ThirdModel:
+      allOf:
+        - $ref: "#/components/schemas/Content"
+        - type: "object"
+          properties:
+            extra_first_attr:
+              description: "The attribute 1 for model 3"
+              type: "string"
+              format: "date-time"
+              example: "2016-01-27T10:52:46.406Z"
+              readOnly: true
+            extra_second_attr:
+              description: "The attribute 2 for model 3"
+              type: integer
+              readOnly: true