Здесь я рассказывал о том, как настроить генерацию в приложении.
В этой статье покажу несколько пример сгенерированного года с использованием композиции и полиморфизма openapi. Здесь можно почитать небольшой раздел документации.

1. Репозиторий

Пример спецификации и сгенерированного кода можно найти здесь. Кроме того по тексту будут оставлены ссылки на конкретные сгенерированные классы и строки в спецификации.

2. discriminator

Здесь можно почитать что такое дискриминатор в концепции openapi. Если вкратце - дискриминатор это именно та вещь, которая позволяет управлять генерацией кода и получать нужный результат при использовании полиморфизма в спецификации.

Далее будут показаны примеры сгенерированного кода на очень простых моделях, без дискриминаторов и с ними.

3.1 OneOf без дискриминатора

Как это выглядит в спецификации:

    OneOfObjectWithoutDiscriminator:
      oneOf:
        - $ref: '#/components/schemas/OneOfObjectWithoutDiscriminatorFirstProperty'
        - $ref: '#/components/schemas/OneOfObjectWithoutDiscriminatorSecondProperty'
    OneOfObjectWithoutDiscriminatorFirstProperty:
      type: object
      required:
        - someProperty
      properties:
        someProperty:
          type: string
    OneOfObjectWithoutDiscriminatorSecondProperty:
      type: object
      required:
        - anotherProperty
      properties:
        anotherProperty:
          type: string

Мы создаём объект, в котором у нас должно лежать два строковых поля. Что мы получаем после генерации: OneOfObjectWithoutDiscriminator, OneOfObjectWithoutDiscriminatorFirstProperty, OneOfObjectWithoutDiscriminatorSecondProperty

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public interface OneOfObjectWithoutDiscriminator {
}
/**
 * OneOfObjectWithoutDiscriminatorFirstProperty
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class OneOfObjectWithoutDiscriminatorFirstProperty implements OneOfObjectWithoutDiscriminator {

  private String someProperty;

...
/**
 * OneOfObjectWithoutDiscriminatorSecondProperty
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class OneOfObjectWithoutDiscriminatorSecondProperty implements OneOfObjectWithoutDiscriminator {

  private String anotherProperty;

...

При получении запроса приложением такая конструкция будет ломаться, потому что мы не сможем определить какой именно реализацией является входящий json.

3.2 OneOf с дискриминатором

Как это выглядит в спецификации:

OneOfObjectWithDiscriminator:
      oneOf:
        - $ref: '#/components/schemas/OneOfObjectWithDiscriminatorFirstProperty'
        - $ref: '#/components/schemas/OneOfObjectWithDiscriminatorSecondProperty'
      discriminator:
        propertyName: propertyType
        mapping:
          first: '#OneOfObjectWithDiscriminatorFirstProperty'
          second: '#OneOfObjectWithDiscriminatorSecondProperty'
    OneOfObjectWithDiscriminatorFirstProperty:
      type: object
      required:
        - propertyType
        - someProperty
      properties:
        propertyType:
          type: string
        someProperty:
          type: string
    OneOfObjectWithDiscriminatorSecondProperty:
      type: object
      required:
        - propertyType
        - anotherProperty
      properties:
        propertyType:
          type: string
        anotherProperty:
          type: string

Мы видим что здесь добавилось обязательное поле, значение которого будет использоваться дискриминатором для определения класса который нужно использовать при получении запроса.

Что мы получаем после генерации: OneOfObjectWithDiscriminator, OneOfObjectWithDiscriminatorFirstProperty, OneOfObjectWithDiscriminatorSecondProperty

@JsonIgnoreProperties(
  value = "propertyType", // ignore manually set propertyType, it will be automatically generated by Jackson during serialization
  allowSetters = true // allows the propertyType to be set during deserialization
)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "propertyType", visible = true)
@JsonSubTypes({
  @JsonSubTypes.Type(value = OneOfObjectWithDiscriminatorFirstProperty.class, name = "first"),
  @JsonSubTypes.Type(value = OneOfObjectWithDiscriminatorSecondProperty.class, name = "second"),
  @JsonSubTypes.Type(value = OneOfObjectWithDiscriminatorFirstProperty.class, name = "OneOfObjectWithDiscriminatorFirstProperty"),
  @JsonSubTypes.Type(value = OneOfObjectWithDiscriminatorSecondProperty.class, name = "OneOfObjectWithDiscriminatorSecondProperty")
})

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public interface OneOfObjectWithDiscriminator {
    public String getPropertyType();
}
/**
 * OneOfObjectWithDiscriminatorFirstProperty
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class OneOfObjectWithDiscriminatorFirstProperty implements OneOfObjectWithDiscriminator {

  private String propertyType;

  private String someProperty;

...
/**
 * OneOfObjectWithDiscriminatorSecondProperty
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class OneOfObjectWithDiscriminatorSecondProperty implements OneOfObjectWithDiscriminator {

  private String propertyType;

  private String anotherProperty;

...

И вот здесь начинает работать обвязка дискриминатором, которая превращается в аннотации JsonSubTypes. Такую генерацию мы уже можем безопасно использовать в нашем приложении.

4.1 AllOf без дискриминатора

Как это выглядит в спецификации:

AllOfObjectWithoutDiscriminator:
      allOf:
        - $ref: '#/components/schemas/AllOfObjectWithoutDiscriminatorFirstProperty'
        - $ref: '#/components/schemas/AllOfObjectWithoutDiscriminatorSecondProperty'
    AllOfObjectWithoutDiscriminatorFirstProperty:
      type: object
      required:
        - someProperty
      properties:
        someProperty:
          type: string
    AllOfObjectWithoutDiscriminatorSecondProperty:
      type: object
      required:
        - anotherProperty
      properties:
        anotherProperty:
          type: string

Результат генерации: AllOfObjectWithoutDiscriminator, AllOfObjectWithoutDiscriminatorFirstProperty, AllOfObjectWithoutDiscriminatorSecondProperty

/**
 * AllOfObjectWithoutDiscriminator
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class AllOfObjectWithoutDiscriminator {

  private String someProperty;

  private String anotherProperty;

...
/**
 * AllOfObjectWithoutDiscriminatorFirstProperty
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class AllOfObjectWithoutDiscriminatorFirstProperty {

  private String someProperty;

...
/**
 * AllOfObjectWithoutDiscriminatorSecondProperty
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class AllOfObjectWithoutDiscriminatorSecondProperty {

  private String anotherProperty;

...

Мы видим что AllOfObjectWithoutDiscriminator является уже не интерфейсом а классом, и содержит в себе сразу оба поля из указанных allOf компонентов.

4.2 AllOf с дискриминатором

Как это выглядит в спецификации:

AllOfObjectWithDiscriminator:
      allOf:
        - $ref: '#/components/schemas/AllOfObjectWithDiscriminatorFirstProperty'
        - $ref: '#/components/schemas/AllOfObjectWithDiscriminatorSecondProperty'
      discriminator:
        propertyName: propertyType
        mapping:
          first: '#AllOfObjectWithDiscriminatorFirstProperty'
          second: '#AllOfObjectWithDiscriminatorSecondProperty'
    AllOfObjectWithDiscriminatorFirstProperty:
      type: object
      required:
        - propertyType
        - someProperty
      properties:
        propertyType:
          type: string
        someProperty:
          type: string
    AllOfObjectWithDiscriminatorSecondProperty:
      type: object
      required:
        - propertyType
        - anotherProperty
      properties:
        propertyType:
          type: string
        anotherProperty:
          type: string

Результат генерации: AllOfObjectWithDiscriminator, AllOfObjectWithDiscriminatorFirstProperty, AllOfObjectWithDiscriminatorSecondProperty

/**
 * AllOfObjectWithDiscriminator
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor


@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class AllOfObjectWithDiscriminator {

  private String propertyType;

  private String someProperty;

  private String anotherProperty;

...
/**
 * AllOfObjectWithDiscriminatorFirstProperty
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class AllOfObjectWithDiscriminatorFirstProperty {

  private String propertyType;

  private String someProperty;

...
/**
 * AllOfObjectWithDiscriminatorSecondProperty
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class AllOfObjectWithDiscriminatorSecondProperty {

  private String propertyType;

  private String anotherProperty;

...

Мы видим что генерация с дискриминатором не отличается от генерации без дискриминатора. Поэтому в случае с allOf, который является инструментом композиции в рамках openapi, можно пользоваться как тем, так и другим вариантом.

5.1 AnyOf без дискриминатора

Как это выглядит в спецификации:

AnyOfObjectWithoutDiscriminator:
      anyOf:
        - $ref: '#/components/schemas/AnyOfObjectWithoutDiscriminatorFirstProperty'
        - $ref: '#/components/schemas/AnyOfObjectWithoutDiscriminatorSecondProperty'
    AnyOfObjectWithoutDiscriminatorFirstProperty:
      type: object
      required:
        - someProperty
      properties:
        someProperty:
          type: string
    AnyOfObjectWithoutDiscriminatorSecondProperty:
      type: object
      required:
        - anotherProperty
      properties:
        anotherProperty:
          type: string

Результат генерации: AnyOfObjectWithoutDiscriminator, AnyOfObjectWithoutDiscriminatorFirstProperty, AnyOfObjectWithoutDiscriminatorSecondProperty

/**
 * AnyOfObjectWithoutDiscriminator
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class AnyOfObjectWithoutDiscriminator {

  private String someProperty;

  private String anotherProperty;

...
/**
 * AnyOfObjectWithoutDiscriminatorFirstProperty
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class AnyOfObjectWithoutDiscriminatorFirstProperty {

  private String someProperty;

...
/**
 * AnyOfObjectWithoutDiscriminatorSecondProperty
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class AnyOfObjectWithoutDiscriminatorSecondProperty {

  private String anotherProperty;

...

5.2 AnyOf с дискриминатором

Как это выглядит в спецификации:

AnyOfObjectWithDiscriminator:
      anyOf:
        - $ref: '#/components/schemas/AnyOfObjectWithDiscriminatorFirstProperty'
        - $ref: '#/components/schemas/AnyOfObjectWithDiscriminatorSecondProperty'
      discriminator:
        propertyName: PropertyType
        mapping:
          first: '#AnyOfObjectWithDiscriminatorFirstProperty'
          second: '#AnyOfObjectWithDiscriminatorSecondProperty'
    AnyOfObjectWithDiscriminatorFirstProperty:
      type: object
      required:
        - propertyType
        - someProperty
      properties:
        propertyType:
          type: string
        someProperty:
          type: string
    AnyOfObjectWithDiscriminatorSecondProperty:
      type: object
      required:
        - propertyType
        - anotherProperty
      properties:
        propertyType:
          type: string
        anotherProperty:
          type: string

Результат генерации: AnyOfObjectWithDiscriminator, AnyOfObjectWithDiscriminatorFirstProperty, AnyOfObjectWithDiscriminatorSecondProperty

/**
 * AnyOfObjectWithDiscriminator
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor

@JsonIgnoreProperties(
  value = "PropertyType", // ignore manually set PropertyType, it will be automatically generated by Jackson during serialization
  allowSetters = true // allows the PropertyType to be set during deserialization
)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "PropertyType", visible = true)
@JsonSubTypes({
  @JsonSubTypes.Type(value = AnyOfObjectWithDiscriminatorFirstProperty.class, name = "first"),
  @JsonSubTypes.Type(value = AnyOfObjectWithDiscriminatorSecondProperty.class, name = "second"),
  @JsonSubTypes.Type(value = AnyOfObjectWithDiscriminatorFirstProperty.class, name = "AnyOfObjectWithDiscriminatorFirstProperty"),
  @JsonSubTypes.Type(value = AnyOfObjectWithDiscriminatorSecondProperty.class, name = "AnyOfObjectWithDiscriminatorSecondProperty")
})

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class AnyOfObjectWithDiscriminator {

  private String propertyType;

  private String someProperty;

  private String anotherProperty;

...
/**
 * AnyOfObjectWithDiscriminatorFirstProperty
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class AnyOfObjectWithDiscriminatorFirstProperty {

  private String propertyType;

  private String someProperty;

...
/**
 * AnyOfObjectWithDiscriminatorSecondProperty
 */
@lombok.Builder(toBuilder = true)
@lombok.RequiredArgsConstructor
@lombok.AllArgsConstructor

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-11-19T15:45:21.742670500+03:00[Europe/Moscow]")
public class AnyOfObjectWithDiscriminatorSecondProperty {

  private String propertyType;

  private String anotherProperty;

...

Результат аналогичный с oneOf. При это если мы используем oneOf, валидация ограничивает входящий запрос до одного из описанных вариантов, в то время как anyOf позволяет принять от одного до всех вариантов.

6. P.S.

AnyOf, AllOf, OneOf можно комбинировать и составлять достаточно сложные конструкции для решения ваших задач.

Иногда одного и того же необходимого результата генерации можно добиться разными способами описания спецификации.

Комментарии (2)


  1. AlexSteelax
    19.11.2023 20:41

    Господи, спасибо тебе за протобаф.


    1. dph
      19.11.2023 20:41

      Да ладно, в протобафе есть Any и несовместимость между версиями, это еще хуже.