Skip to content

yojo-generator/generator

Repository files navigation

Yojo β€” AsyncAPI β†’ Java DTO Generator

Maven Central Gradle Plugin Portal JDK 17+ License AsyncAPI Javadoc Build

Java DTO generator for AsyncAPI β€” transforms YAML specifications into production-ready Java POJOs. Supports Lombok annotations (@Data, @Builder, @Value, @Slf4j), Jackson annotations (@JsonProperty, @JsonFormat, @JsonInclude, @JsonIgnore, @JsonTypeInfo, @JsonSubTypes, @JsonValue/@JsonCreator), Jakarta Bean Validation (@NotBlank, @Email, @Pattern), and manual toString/equals/hashCode. Schema-level x-json-naming: SNAKE_CASE auto-generates @JsonProperty for all fields. Works with AsyncAPI v2.0/v2.6/v3.0 and schema-driven YAML contracts.


Table of Contents


Quick Start

1. Add the Gradle plugin

plugins {
    id("io.github.yojo-generator.gradle-plugin") version "<latest>"
}

2. Configure

yojo {
    configurations {
        create("main") {
            specificationProperties {
                register("api") {
                    specName = "order-service.yaml"
                    inputDirectory = file("contract").absolutePath
                    outputDirectory = layout.buildDirectory
                        .dir("generated/sources/yojo/com/example/api").get().asFile.absolutePath
                    packageLocation = "com.example.api"
                }
            }
        }
    }
}

3. Run

./gradlew generateClasses

Generated DTOs appear in build/generated/sources/yojo/.


Supported Specifications

Specification Status Notes
AsyncAPI v2.0 / v2.6 βœ… Full Primary target β€” all features supported
AsyncAPI v3.0 (RC) βœ… Experimental operations, channels, messages; payload: { schema: {...} }
OpenAPI 3.x ❌ Not supported Consider OpenAPI Generator

Installation

Gradle (recommended)

plugins {
    id("io.github.yojo-generator.gradle-plugin") version "<latest>"
}

πŸ”— Gradle Plugin Portal Β· Plugin source

Maven

<dependency>
    <groupId>io.github.yojo-generator</groupId>
    <artifactId>generator</artifactId>
    <version><latest></version>
</dependency>
πŸ“¦ Full Gradle configuration example
yojo {
    configurations {
        create("main") {
            specificationProperties {
                register("api") {
                    specName("test.yaml")
                    inputDirectory(layout.projectDirectory.dir("contract").asFile.absolutePath)
                    outputDirectory(layout.buildDirectory.dir("generated/sources/yojo/com/example/api")
                        .get().asFile.absolutePath)
                    packageLocation("com.example.api")
                }
                register("one-more-api") {
                    specName("test.yaml")
                    inputDirectory(layout.projectDirectory.dir("contract").asFile.absolutePath)
                    outputDirectory(layout.buildDirectory.dir("generated/sources/yojo/oneMoreApi")
                        .get().asFile.absolutePath)
                    packageLocation("oneMoreApi")
                }
            }
            springBootVersion("3.2.0")
            lombok {
                enable(true)
                allArgsConstructor(true)
                noArgsConstructor(true)
                accessors {
                    enable(true)
                    fluent(false)
                    chain(true)
                }
                equalsAndHashCode {
                    enable(true)
                    callSuper(false)
                }
                builder {
                    enable(true)
                    singular(true)
                    builderDefault(true)
                }
            }
        }
    }
}

Usage Examples

Collections

listOfLongs:
  type: array
  items:
    type: integer
    format: int64
    x-realization: ArrayList
setOfDates:
  type: array
  format: set
  items:
    type: string
    format: date
    x-realization: HashSet
private List<Long> listOfLongs = new ArrayList<>();
private Set<LocalDate> setOfDates = new HashSet<>();

⚠️ The old realization key is deprecated. Use x-realization instead.


Maps with UUID keys

mapUUIDCustomObject:
  type: object
  format: uuid
  additionalProperties:
    $ref: '#/components/schemas/User'
private Map<UUID, User> mapUUIDCustomObject;

Enums with descriptions (x-enumNames)

Result:
  type: object
  enum:
    - SUCCESS
    - DECLINE
    - error-case
  x-enumNames:
    SUCCESS: "Operation succeeded"
    DECLINE: "Declined by policy"
    error-case: "Legacy lowercase"
public enum Result {
    SUCCESS("Operation succeeded"),
    DECLINE("Declined by policy"),
    ERROR_CASE("Legacy lowercase");

    private final String value;
    Result(String value) { this.value = value; }
    public String getValue() { return value; }
}

βœ… Automatic naming: error-case β†’ ERROR_CASE, class β†’ CLASS_FIELD


Enums with wire values (x-enumValues)

OrderStatus:
  type: object
  enum:
    - PENDING
    - CONFIRMED
    - CANCELLED
  x-enumValues:
    PENDING: "P"
    CONFIRMED: "C"
    CANCELLED: "X"
public enum OrderStatus {
    PENDING("P"),
    CONFIRMED("C"),
    CANCELLED("X");

    private final String value;

    OrderStatus(String value) {
        this.value = value;
    }

    @JsonValue
    public String getValue() {
        return value;
    }

    @JsonCreator
    public static OrderStatus fromValue(String value) {
        for (OrderStatus v : OrderStatus.values()) {
            if (v.value.equals(value)) {
                return v;
            }
        }
        throw new IllegalArgumentException("Unknown enum value: " + value);
    }
}

Generates @JsonValue on the getter and @JsonCreator static factory for Jackson serialization/deserialization.

Enum default fallback (x-enumDefault)

When combined with x-enumValues, setting x-enumDefault: true adds an UNKNOWN_DEFAULT_YOJO fallback constant. Instead of throwing IllegalArgumentException, the fromValue() method returns this sentinel for unmapped wire values:

StatusWithDefault:
  type: object
  enum:
    - STARTED
    - STOPPED
  x-enumValues:
    STARTED: "S"
    STOPPED: "T"
  x-enumDefault: true
public enum StatusWithDefault {
    STARTED("S"),
    STOPPED("T"),
    UNKNOWN_DEFAULT_YOJO("UNKNOWN");   // ← fallback

    // ... @JsonValue, @JsonCreator as above ...

    @JsonCreator
    public static StatusWithDefault fromValue(String value) {
        for (StatusWithDefault v : StatusWithDefault.values()) {
            if (v.value.equals(value)) {
                return v;
            }
        }
        return UNKNOWN_DEFAULT_YOJO;   // ← no exception thrown
    }
}

BigDecimal with @Digits (prefer multipleOf)

bigDecimalValue:
  type: number
  format: big-decimal
  multipleOf: 0.01   # β†’ fraction = 2
bigDecimalValue2:
  type: number
  format: big-decimal
  multipleOf: 10.0   # β†’ integer = 2, fraction = 1
@Digits(integer = 1, fraction = 2)
private BigDecimal bigDecimalValue;

@Digits(integer = 2, fraction = 1)
private BigDecimal bigDecimalValue2;

⚠️ digits: "integer = 2, fraction = 2" is legacy. Prefer multipleOf β€” it is standards-compliant.
Use x-digits if you need the explicit form (see Supported Attributes).


Polymorphism (oneOf, allOf)

polymorph:
  oneOf:
    - $ref: '#/components/schemas/StatusOnly'
    - $ref: '#/components/schemas/StatusWithCode'
public class PolymorphStatusOnlyStatusWithCode {
    private String status;
    private Integer code;
}

Discriminator with const support

Pet:
  type: object
  discriminator: petType
  properties:
    name:
      type: string
    petType:
      type: string

Cat:
  allOf:
    - $ref: '#/components/schemas/Pet'
  properties:
    huntingSkill:
      type: string

Dog:
  allOf:
    - $ref: '#/components/schemas/Pet'
  properties:
    packSize:
      type: integer

StickInsect:
  allOf:
    - $ref: '#/components/schemas/Pet'
    - type: object
      properties:
        petType:
          const: StickBug   # ← Overrides default discriminator value
        color:
          type: string
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY,
              property = "petType", visible = true)
@JsonSubTypes({
    @JsonSubTypes.Type(value = Cat.class, name = "Cat"),
    @JsonSubTypes.Type(value = Dog.class, name = "Dog"),
    @JsonSubTypes.Type(value = StickInsect.class, name = "StickBug")
})
public class Pet {
    private String name;
    @JsonTypeId
    private String petType;
}

public class Cat extends Pet {
    private String huntingSkill;
}

public class Dog extends Pet {
    private Integer packSize;
}

public class StickInsect extends Pet {
    private String color;   // petType inherited from Pet, not duplicated
}

Builder pattern (@Builder / manual Builder)

Yojo supports the builder pattern in two modes:

With Lombok β€” generates @Builder, @Singular, and @Builder.Default annotations. Without Lombok β€” generates a full static inner Builder class with fluent setters, singular adders, and build() method.

DSL configuration (build.gradle)

lombok {
    builder {
        enable = true        // Generate @Builder or manual Builder class
        singular = true      // Add @Singular/adder methods for List/Set fields
        builderDefault = true // Apply @Builder.Default to fields with default values
    }
}

YAML schema-level override (x-lombok)

MyDto:
  type: object
  x-lombok:
    builder:
      enable: true
      singular: true
      builderDefault: true
  properties:
    names:
      type: array
      items:
        type: string
      description: Gets @Singular("name")
    scores:
      type: array
      format: set
      items:
        type: integer
      description: Gets @Singular("score")
    message:
      type: string
      default: Hello
      description: Gets @Builder.Default
    count:
      type: integer
      description: Regular field (no builder annotation)

Generated output (with Lombok)

@Builder
public class MyDto {

    @Singular("name")
    private List<String> names;

    @Singular("score")
    private Set<Integer> scores;

    @Builder.Default
    private String message = "Hello";

    private Integer count;
}

Generated output (without Lombok)

public class MyDto {

    private List<String> names;
    private Set<Integer> scores;
    private String message = "Hello";
    private Integer count;

    // getters/setters ...

    private MyDto(Builder builder) {
        this.names = builder.names;
        this.scores = builder.scores;
        this.message = builder.message;
        this.count = builder.count;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {
        private List<String> names = new ArrayList<>();
        private Set<Integer> scores = new HashSet<>();
        private String message = "Hello";
        private Integer count;

        public Builder names(List<String> names) { ... return this; }
        public Builder name(String name) { this.names.add(name); return this; }  // singular
        public Builder score(Integer score) { this.scores.add(score); return this; }
        public MyDto build() { return new MyDto(this); }
    }
}

πŸ’‘ The singular name is derived automatically by removing the trailing s from the field name (e.g., names β†’ "name", scores β†’ "score"). This matches Lombok's @Singular convention.

Builder DSL reference

Option Type Default Description
enable boolean false Generate @Builder (Lombok) or manual Builder class (no Lombok)
singular boolean true Add @Singular / singular adder methods for List/Set fields
builderDefault boolean true Apply @Builder.Default / propagate default values to Builder

Custom annotations (x-class-annotation, x-field-annotation)

Attribute YAML β†’ Java
x-class-annotation
x-class-annotation:  - com.example.MyAnnotation  - com.example.Another("param")
@MyAnnotation@Another("param")public class MySchema { ... }
x-field-annotation
myField:  type: string  x-field-annotation:    - com.example.MyFieldAnnotation
@MyFieldAnnotationprivate String myField;

Jackson annotations (x-json-property, x-json-format, x-json-include, x-json-ignore, x-json-naming)

Yojo supports fine-grained Jackson serialization annotations via x-json-* attributes on YAML schemas and properties.

Field-level annotations

YAML Attribute β†’ Java Annotation Description
x-json-property @JsonProperty("wireName") Custom serialized name for the field
x-json-format @JsonFormat(pattern = "...", shape = ...) Date/time format pattern
x-json-ignore @JsonIgnore Exclude field from serialization/deserialization
x-json-include @JsonInclude(Include.NON_NULL) Class-level inclusion rule

Schema-level naming strategy

Setting x-json-naming: SNAKE_CASE at the schema level automatically generates @JsonProperty("snake_name") for all fields in that schema, converting camelCase field names to snake_case. Individual x-json-property overrides take precedence.

Example

JacksonTestSchema:
  type: object
  x-json-naming: SNAKE_CASE
  x-json-include: NON_NULL
  properties:
    userId:
      type: integer
      format: int64
      description: Gets @JsonProperty("user_id") from naming strategy
    createdAt:
      type: string
      format: date-time
      x-json-format:
        pattern: "yyyy-MM-dd'T'HH:mm:ss"
        timezone: "UTC"
      description: Gets @JsonFormat on the field
    internalCode:
      type: string
      x-json-ignore: true
      description: Gets @JsonIgnore
    displayName:
      type: string
      x-json-property: display_name_custom
      description: Overrides naming strategy with custom @JsonProperty

Generated output

@JsonInclude(JsonInclude.Include.NON_NULL)
public class JacksonTestSchema {

    @JsonProperty("user_id")
    private Long userId;

    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "UTC")
    @JsonProperty("created_at")
    private OffsetDateTime createdAt;

    @JsonIgnore
    @JsonProperty("internal_code")
    private String internalCode;

    @JsonProperty("display_name_custom")
    private String displayName;
}

πŸ’‘ The schema-level x-json-naming: SNAKE_CASE applies @JsonProperty("snake_name") to every field automatically. Use x-json-property on individual fields to override the auto-generated name.


Final fields (x-final)

ImmutableDto:
  type: object
  properties:
    createdAt:
      type: string
      format: date-time
      x-final: true
    name:
      type: string
@Generated("Yojo")
public class ImmutableDto {

    public ImmutableDto(OffsetDateTime createdAt) {
        this.createdAt = createdAt;
    }

    private final OffsetDateTime createdAt;

    private String name;

    public OffsetDateTime getCreatedAt() {
        return createdAt;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Fields marked with x-final: true are declared as final and must be initialized via constructor. A constructor is generated automatically for all final fields without default values. Lombok's @NoArgsConstructor is skipped when uninitialized final fields exist.


YAML ↔ Java Type Mapping

Scalar types

YAML Java Imports
type: string, format: uuid UUID java.util.UUID
type: object, format: date LocalDate java.time.LocalDate
type: string, format: local-date-time LocalDateTime java.time.LocalDateTime
type: object, format: date-time OffsetDateTime java.time.OffsetDateTime
type: number, format: big-decimal, multipleOf: 0.01 BigDecimal java.math.BigDecimal, @Digits(integer = 1, fraction = 2)
type: string, format: email String @Email (Jakarta/javax)
type: string, format: uri URI java.net.URI
type: integer, format: int64 Long β€”
type: number, format: float Float β€”
type: boolean Boolean β€”

Collections & maps

YAML β†’ Java
type: arrayitems:  type: string
List<String>
type: arrayformat: setitems:  type: integer
Set<Integer>
type: objectadditionalProperties:  type: string
Map<String, String>
type: objectformat: uuidadditionalProperties:  $ref: '#/...'
Map<UUID, User>
type: objectadditionalProperties:  type: array  format: set  items:    type: string
Map<String, Set<String>>

Supported Attributes

All custom Yojo attributes now have x- prefixed equivalents. While the old names still work (backward compatibility), they are deprecated and will log a warning. New contracts should use the x- prefixed names.

Attribute (old, deprecated) Attribute (new, preferred) Scope Type Example Effect
β€” x-realization items, additionalProperties string ArrayList = new ArrayList<>()
realization β€” ↑ ↑ ↑ ↑ (deprecated)
β€” x-digits number string "integer=2, fraction=2" @Digits(...)
digits β€” ↑ ↑ ↑ ↑ (deprecated)
β€” x-additional-format additionalProperties string uuid Custom map key type
additionalFormat β€” ↑ ↑ ↑ ↑ (deprecated)
β€” x-validation-groups schema list [Create.class] @NotBlank(groups = {Create.class})
validationGroups β€” ↑ ↑ ↑ ↑ (deprecated)
β€” x-validation-groups-imports schema list [pkg.Validation] import pkg.Validation;
validationGroupsImports β€” ↑ ↑ ↑ ↑ (deprecated)
β€” x-validate-by-groups schema list [name, email] Annotations only on listed fields
validateByGroups β€” ↑ ↑ ↑ ↑ (deprecated)
β€” x-lombok.* schema/message nested see Gradle config @Data, @Accessors, etc.
lombok.* β€” ↑ ↑ ↑ ↑ (deprecated)
β€” x-extends / x-implements schema/message map x-from-class, x-from-package extends X implements Y
extends / implements β€” ↑ ↑ ↑ ↑ (deprecated)
β€” x-from-class inside x-extends string BaseClass Superclass name
fromClass β€” ↑ ↑ ↑ ↑ (deprecated)
β€” x-from-package inside x-extends string com.example Superclass package
fromPackage β€” ↑ ↑ ↑ ↑ (deprecated)
β€” x-from-interface inside x-implements list [com.example.Ifc] Interface FQN list
fromInterface β€” ↑ ↑ ↑ ↑ (deprecated)
β€” x-remove-schema message.payload boolean true Skip DTO generation for $ref target
removeSchema β€” ↑ ↑ ↑ ↑ (deprecated)
β€” x-methods schema (interface) map see interface example Interface method definitions
methods β€” ↑ ↑ ↑ ↑ (deprecated)
β€” x-imports schema (interface) list [java.util.List] Interface-level imports
imports β€” ↑ ↑ ↑ ↑ (deprecated)
β€” x-definition inside method entry string "void doWork();" Method signature in interfaces
definition β€” ↑ ↑ ↑ ↑ (deprecated)
β€” x-path-for-generate-message message.payload string io.github.events Custom message package
pathForGenerateMessage β€” ↑ ↑ ↑ ↑ (deprecated)
primitive β€” field boolean true int, boolean, long
multipleOf β€” number number 0.01 @Digits(integer = 1, fraction = 2)
const β€” schema, field string "StickBug" Override discriminator value
discriminator β€” schema string "petType" @JsonTypeInfo + @JsonSubTypes
format: interface β€” schema β€” β€” interface X { ... }
format: existing β€” field name, package β€” Import existing class
x-enumNames β€” enum map SUCCESS: "Ok" Enum with description field
x-enumValues β€” enum map ACTIVE: "A" Enum with wire values β†’ @JsonValue/@JsonCreator
x-enumDefault β€” enum predicate boolean true Adds UNKNOWN_DEFAULT_YOJO fallback
x-class-annotation β€” schema, message list [com.example.MyAnnotation] Class-level annotations
x-field-annotation β€” field list [com.example.MyAnnotation("v")] Field-level annotations
x-final β€” field boolean true Generates final field declaration
x-json-property β€” field string "display_name" @JsonProperty("display_name") on field
x-json-format β€” field map {pattern: "...", timezone: "UTC"} @JsonFormat(...) on field
x-json-include β€” schema string NON_NULL @JsonInclude(Include.NON_NULL) on class
x-json-ignore β€” field boolean true @JsonIgnore on field
x-json-naming β€” schema string SNAKE_CASE Auto-generates @JsonProperty for all fields

Security

Path traversal prevention

Yojo validates all file paths when writing generated source code to prevent directory traversal attacks. File names are checked for:

  • Path separator characters (/, \)
  • Parent directory references (..)
  • Normalized path containment within the target directory

If a path traversal attempt is detected, a SecurityException is thrown.

CVE-2024-45373 β€” Path traversal vulnerability in JavaFileWriter was identified and fixed in commit 609da11.

Reporting

If you discover a security issue, please contact the maintainer directly (email below) rather than opening a public issue.


Roadmap

Feature Status Description
Discriminator βœ… Done @JsonTypeInfo, @JsonSubTypes for polymorphism
@JsonTypeId on fields βœ… Done Discriminator field annotation in subtypes
Discriminator const βœ… Done Override default discriminator value via const
Jackson annotations βœ… Done (4.6.0) @JsonProperty, @JsonFormat, @JsonInclude, @JsonIgnore, @JsonValue/@JsonCreator (via x-enumValues), and x-json-naming: SNAKE_CASE
x- prefixed attributes βœ… Done (4.3.0) All custom attributes deprecated in favour of x- equivalents
AsyncAPI spec validation 🚧 Planned Validate $ref, type, format, detect circular refs
Lombok extensions βœ… Done (4.4.0) @Builder, @Singular, @Builder.Default + manual Builder class
OpenAPI 3.1 support 🚧 Planned After AsyncAPI 3.0 stabilizes
Freemarker templates 🚧 Planned Customizable code generation for enterprises

Contributing

  1. Fork the repository
  2. Create a feature branch from develop
  3. Make changes β€” each step in a separate branch
  4. Run tests: ./gradlew test
  5. Open a Pull Request

Development requirements

  • JDK 17+
  • Gradle (wrapper included)

All generated output is tested against expected files in src/test/resources/example/expected/.


Contact

Got an AsyncAPI contract? Send it over β€” I'll make a proof of concept.


License

Distributed under the Apache License 2.0. See LICENSE.md.


AsyncAPI β†’ Java β€” fast, precise, zero manual work.

Packages

 
 
 

Contributors

Languages