
Skir's Java code generator
Official plugin for generating Java code from .skir files.
Set up
In your skir.yml file, add the following snippet under generators:
- mod: skir-java-gen
outDir: ./src/main/java/skirout
config: {}
The generated Java code has a runtime dependency on build.skir:skir-client. Add this line to your build.gradle file in the dependencies section:
implementation 'build.skir:skir-client:latest.release'
For more information, see this Java project example.
Java generated code guide
The examples below are for the code generated from this .skir file.
Referring to generated symbols
import skirout.user.User;
import skirout.user.UserRegistry;
import skirout.user.SubscriptionStatus;
import skirout.user.Constants;
Struct classes
skir generates a deeply immutable Java class for every struct in the .skir file.
final User john =
User.builder()
.setName("John Doe")
.setPets(
List.of(
User.Pet.builder()
.setHeightInMeters(1.0f)
.setName("Dumbo")
.setPicture("🐘")
.build()))
.setQuote("Coffee is just a socially acceptable form of rage.")
.setSubscriptionStatus(SubscriptionStatus.FREE)
.setUserId(42)
.build();
assert john.name().equals("John Doe");
final User jane = User.partialBuilder().setUserId(43).setName("Jane Doe").build();
assert jane.quote().equals("");
assert jane.pets().equals(List.of());
assert User.DEFAULT.name().equals("");
assert User.DEFAULT.userId() == 0;
Creating modified copies
final User evilJohn =
john.toBuilder()
.setName("Evil John")
.setQuote("I solemnly swear I am up to no good.")
.build();
assert evilJohn.name().equals("Evil John");
assert evilJohn.userId() == 42;
Enum classes
skir generates a deeply immutable Java class for every enum in the .skir file. This class is not a Java enum, although the syntax for referring to constants is similar.
The definition of the SubscriptionStatus enum in the .skir file is:
enum SubscriptionStatus {
FREE;
trial: Trial;
PREMIUM;
}
Making enum values
final List<SubscriptionStatus> someStatuses =
List.of(
SubscriptionStatus.UNKNOWN,
SubscriptionStatus.FREE,
SubscriptionStatus.PREMIUM,
SubscriptionStatus.wrapTrial(
SubscriptionStatus.Trial.builder()
.setStartTime(Instant.now())
.build()));
Conditions on enums
assert john.subscriptionStatus().equals(SubscriptionStatus.FREE);
assert jane.subscriptionStatus().equals(SubscriptionStatus.UNKNOWN);
final Instant now = Instant.now();
final SubscriptionStatus trialStatus =
SubscriptionStatus.wrapTrial(
SubscriptionStatus.Trial.builder()
.setStartTime(now)
.build());
assert trialStatus.kind() == SubscriptionStatus.Kind.TRIAL_WRAPPER;
assert trialStatus.asTrial().startTime() == now;
Branching on enum variants
final Function<SubscriptionStatus, String> getInfoText =
status ->
switch (status.kind()) {
case FREE_CONST -> "Free user";
case PREMIUM_CONST -> "Premium user";
case TRIAL_WRAPPER -> "On trial since " + status.asTrial().startTime();
case UNKNOWN -> "Unknown subscription status";
default -> throw new AssertionError("Unreachable");
};
System.out.println(getInfoText.apply(john.subscriptionStatus()));
final SubscriptionStatus.Visitor<String> infoTextVisitor =
new SubscriptionStatus.Visitor<>() {
@Override
public String onFree() {
return "Free user";
}
@Override
public String onPremium() {
return "Premium user";
}
@Override
public String onTrial(SubscriptionStatus.Trial trial) {
return "On trial since " + trial.startTime();
}
@Override
public String onUnknown() {
return "Unknown subscription status";
}
};
System.out.println(john.subscriptionStatus().accept(infoTextVisitor));
Serialization
Every frozen struct class and enum class has a static readonly SERIALIZER property which can be used for serializing and deserializing instances of the class.
final Serializer<User> serializer = User.SERIALIZER;
final String johnDenseJson = serializer.toJsonCode(john);
System.out.println(johnDenseJson);
System.out.println(serializer.toJsonCode(john, JsonFlavor.READABLE));
final ByteString johnBytes = serializer.toBytes(john);
System.out.println(johnBytes);
Deserialization
final User reserializedJohn = serializer.fromJsonCode(johnDenseJson);
assert reserializedJohn.equals(john);
final User reserializedEvilJohn =
serializer.fromJsonCode(
serializer.toJsonCode(john, JsonFlavor.READABLE));
assert reserializedEvilJohn.equals(evilJohn);
assert serializer.fromBytes(johnBytes).equals(john);
Primitive serializers
assert Serializers.bool().toJsonCode(true).equals("1");
assert Serializers.int32().toJsonCode(3).equals("3");
assert Serializers.int64().toJsonCode(9223372036854775807L).equals("\"9223372036854775807\"");
assert Serializers.javaHash64()
.toJsonCode(new BigInteger("18446744073709551615"))
.equals("\"18446744073709551615\"");
assert Serializers.timestamp()
.toJsonCode(Instant.ofEpochMilli(1743682787000L))
.equals("1743682787000");
assert Serializers.float32().toJsonCode(3.14f).equals("3.14");
assert Serializers.float64().toJsonCode(3.14).equals("3.14");
assert Serializers.string().toJsonCode("Foo").equals("\"Foo\"");
assert Serializers.bytes()
.toJsonCode(ByteString.of((byte) 1, (byte) 2, (byte) 3))
.equals("\"AQID\"");
Composite serializers
assert Serializers.javaOptional(Serializers.string())
.toJsonCode(java.util.Optional.of("foo"))
.equals("\"foo\"");
assert Serializers.javaOptional(Serializers.string())
.toJsonCode(java.util.Optional.empty())
.equals("null");
assert Serializers.list(Serializers.bool())
.toJsonCode(List.of(true, false))
.equals("[1,0]");
Frozen lists and copies
final List<User.Pet> pets = new ArrayList<>();
pets.add(
User.Pet.builder()
.setHeightInMeters(0.25f)
.setName("Fluffy")
.setPicture("🐶")
.build());
pets.add(
User.Pet.builder()
.setHeightInMeters(0.5f)
.setName("Fido")
.setPicture("🐻")
.build());
final User jade =
User.partialBuilder()
.setName("Jade")
.setPets(pets)
.build();
assert pets.equals(jade.pets());
assert pets != jade.pets();
final User jack =
User.partialBuilder()
.setName("Jack")
.setPets(jade.pets())
.build();
assert jack.pets() == jade.pets();
Keyed lists
final UserRegistry userRegistry =
UserRegistry.builder().setUsers(List.of(john, jane, evilJohn)).build();
assert userRegistry.users().findByKey(43) == jane;
assert userRegistry.users().findByKey(42) == evilJohn;
assert userRegistry.users().findByKey(100) == null;
Constants
System.out.println(Constants.TARZAN);
SkirRPC services
Starting a SkirRPC service on an HTTP server
Full example here.
Sending RPCs to a SkirRPC service
Full example here.
Reflection
Reflection allows you to inspect a skir type at runtime.
import build.skir.reflection.StructDescriptor;
import build.skir.reflection.TypeDescriptor;
System.out.println(
User.TYPE_DESCRIPTOR
.getFields()
.stream()
.map((field) -> field.getName())
.toList());
final TypeDescriptor typeDescriptor =
TypeDescriptor.Companion.parseFromJsonCode(
User.SERIALIZER.typeDescriptor().asJsonCode());
assert typeDescriptor instanceof StructDescriptor;
assert ((StructDescriptor) typeDescriptor).getFields().size() == 5;
System.out.println(
AllStringsToUpperCase.allStringsToUpperCase(
Constants.TARZAN, User.TYPE_DESCRIPTOR));
Java codegen versus Kotlin codegen
While Java and Kotlin code can interoperate seamlessly, skir provides separate code generators for each language to leverage their unique strengths and idioms. For instance, the Kotlin generator utilizes named parameters for struct construction, whereas the Java generator employs the builder pattern.
Although it's technically feasible to use Kotlin-generated code in a Java project (or vice versa), doing so results in an API that feels unnatural and cumbersome in the calling language. For the best developer experience, use the code generator that matches your project's primary language.
Note that both the Java and Kotlin generated code share the same runtime dependency: build.skir:skir-client.