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.
- Quick Start
- Supported Specifications
- Installation
- Usage Examples
- YAML β Java Type Mapping
- Supported Attributes
- Security
- Roadmap
- Contributing
- License
plugins {
id("io.github.yojo-generator.gradle-plugin") version "<latest>"
}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"
}
}
}
}
}./gradlew generateClassesGenerated DTOs appear in build/generated/sources/yojo/.
| 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 |
plugins {
id("io.github.yojo-generator.gradle-plugin") version "<latest>"
}π Gradle Plugin Portal Β· Plugin source
<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)
}
}
}
}
}listOfLongs:
type: array
items:
type: integer
format: int64
x-realization: ArrayList
setOfDates:
type: array
format: set
items:
type: string
format: date
x-realization: HashSetprivate List<Long> listOfLongs = new ArrayList<>();
private Set<LocalDate> setOfDates = new HashSet<>();
β οΈ The oldrealizationkey is deprecated. Usex-realizationinstead.
mapUUIDCustomObject:
type: object
format: uuid
additionalProperties:
$ref: '#/components/schemas/User'private Map<UUID, User> mapUUIDCustomObject;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
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
@JsonValueon the getter and@JsonCreatorstatic factory for Jackson serialization/deserialization.
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: truepublic 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
}
}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. PrefermultipleOfβ it is standards-compliant.
Usex-digitsif you need the explicit form (see Supported Attributes).
polymorph:
oneOf:
- $ref: '#/components/schemas/StatusOnly'
- $ref: '#/components/schemas/StatusWithCode'public class PolymorphStatusOnlyStatusWithCode {
private String status;
private Integer code;
}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
}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.
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
}
}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)@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;
}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
sfrom the field name (e.g.,namesβ"name",scoresβ"score"). This matches Lombok's@Singularconvention.
| 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 |
| 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; |
Yojo supports fine-grained Jackson serialization annotations via x-json-* attributes on YAML schemas and properties.
| 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 |
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.
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@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_CASEapplies@JsonProperty("snake_name")to every field automatically. Usex-json-propertyon individual fields to override the auto-generated name.
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: trueare declared asfinaland must be initialized via constructor. A constructor is generated automatically for all final fields without default values. Lombok's@NoArgsConstructoris skipped when uninitialized final fields exist.
| 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 |
β |
| 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>> |
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 |
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
JavaFileWriterwas identified and fixed in commit609da11.
If you discover a security issue, please contact the maintainer directly (email below) rather than opening a public issue.
| 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 |
- Fork the repository
- Create a feature branch from
develop - Make changes β each step in a separate branch
- Run tests:
./gradlew test - Open a Pull Request
- JDK 17+
- Gradle (wrapper included)
All generated output is tested against expected files in src/test/resources/example/expected/.
- Author: Vladimir Morozkin
- Email:
jvmorozkin@gmail.com - Telegram: @vmorozkin
- GitHub: yojo-generator
Got an AsyncAPI contract? Send it over β I'll make a proof of concept.
Distributed under the Apache License 2.0. See LICENSE.md.
AsyncAPI β Java β fast, precise, zero manual work.