diff --git a/crd-generator/api-v2/pom.xml b/crd-generator/api-v2/pom.xml
index e0bb7794270..2e19caa12ee 100644
--- a/crd-generator/api-v2/pom.xml
+++ b/crd-generator/api-v2/pom.xml
@@ -77,5 +77,46 @@
lombok
test
+
+ org.assertj
+ assertj-core
+ test
+
+
+ com.github.victools
+ jsonschema-generator
+ ${victools-jsonschema.version}
+ test
+
+
+ com.github.victools
+ jsonschema-module-jackson
+ ${victools-jsonschema.version}
+ test
+
+
+ com.github.victools
+ jsonschema-module-jakarta-validation
+ ${victools-jsonschema.version}
+ test
+
+
+ com.github.victools
+ jsonschema-module-swagger-2
+ ${victools-jsonschema.version}
+ test
+
+
+ jakarta.validation
+ jakarta.validation-api
+ ${jakarta-validation.version}
+ test
+
+
+ io.swagger.core.v3
+ swagger-annotations
+ ${swagger-annotations.version}
+ test
+
diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/AdditionalPrinterColumn.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/AdditionalPrinterColumn.java
new file mode 100644
index 00000000000..570c16845e7
--- /dev/null
+++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/AdditionalPrinterColumn.java
@@ -0,0 +1,72 @@
+package io.fabric8.crdv2.generator.alt;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation that allows additionalPrinterColumns entries to be created with arbitrary JSONPaths.
+ */
+@Repeatable(AdditionalPrinterColumn.List.class)
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface AdditionalPrinterColumn {
+
+ String name();
+
+ String jsonPath();
+
+ AdditionalPrinterColumn.Type type() default Type.STRING;
+
+ AdditionalPrinterColumn.Format format() default Format.NONE;
+
+ String description() default "";
+
+ int priority() default 0;
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ @interface List {
+
+ AdditionalPrinterColumn[] value();
+ }
+
+ // https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#type
+ public static enum Type {
+
+ STRING("string"),
+ INTEGER("integer"),
+ NUMBER("number"),
+ BOOLEAN("boolean"),
+ DATE("date");
+
+ public final String value;
+
+ Type(String value) {
+ this.value = value;
+ }
+ }
+
+ // https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#format
+ public static enum Format {
+
+ NONE(""),
+ INT32("int32"),
+ INT64("int64"),
+ FLOAT("float"),
+ DOUBLE("double"),
+ BYTE("byte"),
+ BINARY("binary"),
+ DATE("date"),
+ DATE_TIME("date-time"),
+ PASSWORD("password");
+
+ public final String value;
+
+ Format(String value) {
+ this.value = value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/ConfigurableCrdGenerator.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/ConfigurableCrdGenerator.java
new file mode 100644
index 00000000000..abfabc78d5a
--- /dev/null
+++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/ConfigurableCrdGenerator.java
@@ -0,0 +1,156 @@
+package io.fabric8.crdv2.generator.alt;
+
+import static io.fabric8.crdv2.generator.alt.GeneratorUtils.convertValue;
+import static io.fabric8.crdv2.generator.alt.GeneratorUtils.getPrinterColumns;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.victools.jsonschema.generator.Module;
+import com.github.victools.jsonschema.generator.Option;
+import com.github.victools.jsonschema.generator.OptionPreset;
+import com.github.victools.jsonschema.generator.SchemaGenerator;
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
+import com.github.victools.jsonschema.generator.SchemaVersion;
+import com.github.victools.jsonschema.module.jackson.JacksonModule;
+import com.github.victools.jsonschema.module.jackson.JacksonOption;
+
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition;
+import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.Kind;
+import io.fabric8.kubernetes.model.annotation.Plural;
+import io.fabric8.kubernetes.model.annotation.Singular;
+import io.fabric8.kubernetes.model.annotation.Version;
+
+/**
+ * Alternative CRD generator implementation that uses victools/jsonschema-generator and allows schema generation to be
+ * customized via modules.
+ *
+ *
+ * Support for Jakarta Validation API annotations can be enabled using victools/jsonschema-module-jakarta-validation.
+ *
+ *
+ * Support for Swagger annotations can be enabled using victools/jsonschema-module-swagger-2.
+ */
+public class ConfigurableCrdGenerator {
+
+ private final List modules = new ArrayList<>();
+ private SchemaGenerator schemaGenerator = null;
+
+ public ConfigurableCrdGenerator register(Module module) {
+ if (schemaGenerator != null) {
+ throw new IllegalStateException("Already created schema generator");
+ }
+ modules.add(module);
+ return this;
+ }
+
+ protected SchemaGeneratorConfigBuilder baseConfig() {
+ return new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_6, OptionPreset.PLAIN_JSON)
+ .with(Option.ENUM_KEYWORD_FOR_SINGLE_VALUES)
+ .with(Option.INLINE_ALL_SCHEMAS)
+ .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES)
+ .with(new JacksonModule(
+ JacksonOption.FLATTENED_ENUMS_FROM_JSONPROPERTY,
+ JacksonOption.RESPECT_JSONPROPERTY_ORDER))
+ .with(new Fabric8Module());
+ }
+
+ protected synchronized SchemaGenerator schemaGenerator() {
+ if (schemaGenerator == null) {
+ SchemaGeneratorConfigBuilder configBuilder = baseConfig();
+ for (Module module : modules) {
+ configBuilder = configBuilder.with(module);
+ }
+ configBuilder = configBuilder.with(new CrdComplianceModule());
+ schemaGenerator = new SchemaGenerator(configBuilder.build());
+ }
+ return schemaGenerator;
+ }
+
+ public ObjectNode getSchema(Type type) {
+ ObjectNode schema = schemaGenerator().generateSchema(type);
+ schema.remove("$schema");
+ return schema;
+ }
+
+ public CustomResourceDefinition generateCrd(Class extends CustomResource> crdClass) {
+ Type genericClass = crdClass.getGenericSuperclass();
+ if (!(genericClass instanceof ParameterizedType)) {
+ throw new IllegalArgumentException(crdClass.getName() + " is not a parameterize type");
+ }
+ Type[] typeParams = ((ParameterizedType) genericClass).getActualTypeArguments();
+ if (typeParams.length != 2) {
+ throw new IllegalArgumentException(
+ "Unexpected number of type parameters for class " + crdClass.getName() + ": " + typeParams.length);
+ }
+ @SuppressWarnings("unchecked")
+ Class specClass = (Class) typeParams[0];
+ @SuppressWarnings("unchecked")
+ Class statusClass = (Class) typeParams[1];
+ return generateCrd(crdClass, specClass, statusClass);
+ }
+
+ public CustomResourceDefinition generateCrd(
+ Class extends CustomResource> crdClass,
+ Class specClass, Class statusClass) {
+ String kind = Optional.ofNullable(crdClass.getAnnotation(Kind.class))
+ .map(ann -> ann.value())
+ .orElseGet(crdClass::getSimpleName);
+ String singular = Optional.ofNullable(crdClass.getAnnotation(Singular.class))
+ .map(ann -> ann.value())
+ .orElseGet(() -> kind.toLowerCase(Locale.US));
+ String plural = Optional.ofNullable(crdClass.getAnnotation(Plural.class))
+ .map(ann -> ann.value())
+ .orElseGet(() -> singular + "s");
+ String group = Optional.ofNullable(crdClass.getAnnotation(Group.class))
+ .map(ann -> ann.value())
+ .orElse(crdClass.getPackage().getName());
+ String version = Optional.ofNullable(crdClass.getAnnotation(Version.class))
+ .map(ann -> ann.value())
+ .orElse("v1beta1");
+ String scope = Namespaced.class.isAssignableFrom(crdClass) ? "Namespaced" : "Cluster";
+ JsonNode specSchema = getSchema(specClass);
+ JsonNode statusSchema = getSchema(statusClass);
+ return new CustomResourceDefinition().edit()
+ .withNewMetadata()
+ .withName(plural + "." + group)
+ .endMetadata()
+ .withNewSpec()
+ .withGroup(group)
+ .withNewNames()
+ .withKind(kind)
+ .withPlural(plural)
+ .withSingular(singular)
+ .endNames()
+ .withScope(scope)
+ .addNewVersion()
+ .withName(version)
+ .withAdditionalPrinterColumns(getPrinterColumns(crdClass))
+ .withNewSchema()
+ .withNewOpenAPIV3Schema()
+ .addToProperties("spec", convertValue(specSchema, JSONSchemaProps.class))
+ .addToProperties("status", convertValue(statusSchema, JSONSchemaProps.class))
+ .withType("object")
+ .endOpenAPIV3Schema()
+ .endSchema()
+ .withServed()
+ .withStorage()
+ .withNewSubresources()
+ .withNewStatus()
+ .endStatus()
+ .endSubresources()
+ .endVersion()
+ .endSpec()
+ .build();
+ }
+}
diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/CrdComplianceModule.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/CrdComplianceModule.java
new file mode 100644
index 00000000000..d4ef1149f65
--- /dev/null
+++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/CrdComplianceModule.java
@@ -0,0 +1,43 @@
+package io.fabric8.crdv2.generator.alt;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.victools.jsonschema.generator.Module;
+import com.github.victools.jsonschema.generator.SchemaGenerationContext;
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
+
+/**
+ * Module that enforces compliance with JSONSchema dialect used by CRDs. This should be registered last so that it removes any
+ * restricted attributes added by other modules.
+ */
+public class CrdComplianceModule implements Module {
+
+ // attributes not supported by CRDs according to Kubernetes documentation:
+ // https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation
+ public static final List RESTRICTED_ATTRIBUTES = Collections.unmodifiableList(Arrays.asList(
+ "definitions",
+ "dependencies",
+ "deprecated",
+ "discriminator",
+ "id",
+ "patternProperties",
+ "readOnly",
+ "writeOnly",
+ "xml",
+ "$ref",
+ "uniqueItems"));
+
+ @Override
+ public void applyToConfigBuilder(SchemaGeneratorConfigBuilder builder) {
+ builder.forTypesInGeneral().withTypeAttributeOverride(this::overrideAttributes);
+ builder.forFields().withInstanceAttributeOverride(this::overrideAttributes);
+ builder.forMethods().withInstanceAttributeOverride(this::overrideAttributes);
+ }
+
+ protected void overrideAttributes(ObjectNode attributes, Object scope, SchemaGenerationContext context) {
+ RESTRICTED_ATTRIBUTES.forEach(attributes::remove);
+ }
+}
\ No newline at end of file
diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/EnumDescriptionsModule.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/EnumDescriptionsModule.java
new file mode 100644
index 00000000000..4ca5604ae6d
--- /dev/null
+++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/EnumDescriptionsModule.java
@@ -0,0 +1,63 @@
+package io.fabric8.crdv2.generator.alt;
+
+import java.lang.reflect.Field;
+import java.util.Optional;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.github.victools.jsonschema.generator.Module;
+import com.github.victools.jsonschema.generator.SchemaGenerationContext;
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
+import com.github.victools.jsonschema.generator.TypeScope;
+
+/**
+ * Module that injects descriptions for enum constants declared using the {@code @JsonPropertyDescription} annotation. This
+ * should be registered after any modules that would resolve the property description.
+ */
+public class EnumDescriptionsModule implements Module {
+
+ @Override
+ public void applyToConfigBuilder(SchemaGeneratorConfigBuilder builder) {
+ builder.forTypesInGeneral().withTypeAttributeOverride(this::overrideAttributes);
+ builder.forFields().withInstanceAttributeOverride(this::overrideAttributes);
+ builder.forMethods().withInstanceAttributeOverride(this::overrideAttributes);
+ }
+
+ protected void overrideAttributes(
+ ObjectNode attributes, TypeScope scope, SchemaGenerationContext context) {
+ // if description was resolved for property of type enum, resolve descriptions
+ // for enum constants and add them to the description
+ JsonNode node = attributes.get("description");
+ if (node == null || !node.isTextual()) {
+ return;
+ }
+ Class> clazz = scope.getType().getErasedType();
+ if (!clazz.isEnum()) {
+ return;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (Object obj : clazz.getEnumConstants()) {
+ if (obj instanceof Enum>) {
+ try {
+ Field field = clazz.getField(((Enum>) obj).name());
+ JsonPropertyDescription description = field.getAnnotation(JsonPropertyDescription.class);
+ if (description != null) {
+ String propertyName = Optional.ofNullable(field.getAnnotation(JsonProperty.class))
+ .map(ann -> ann.value())
+ .orElse(obj.toString());
+ sb.append("\n * `").append(propertyName)
+ .append("` - ").append(description.value());
+ }
+ } catch (ReflectiveOperationException e) {
+ // do nothing
+ }
+ }
+ }
+ if (sb.length() != 0) {
+ attributes.set("description", TextNode.valueOf(node.asText() + sb.toString()));
+ }
+ }
+}
\ No newline at end of file
diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/Fabric8Module.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/Fabric8Module.java
new file mode 100644
index 00000000000..c8f2dd85938
--- /dev/null
+++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/Fabric8Module.java
@@ -0,0 +1,120 @@
+package io.fabric8.crdv2.generator.alt;
+
+import static io.fabric8.crdv2.generator.alt.GeneratorUtils.convertValue;
+import static io.fabric8.crdv2.generator.alt.GeneratorUtils.emptyToNull;
+
+import java.lang.annotation.Annotation;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.github.victools.jsonschema.generator.MemberScope;
+import com.github.victools.jsonschema.generator.Module;
+import com.github.victools.jsonschema.generator.SchemaGenerationContext;
+import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
+import com.github.victools.jsonschema.generator.TypeScope;
+
+import io.fabric8.generator.annotation.Default;
+import io.fabric8.generator.annotation.Max;
+import io.fabric8.generator.annotation.Min;
+import io.fabric8.generator.annotation.Nullable;
+import io.fabric8.generator.annotation.Pattern;
+import io.fabric8.generator.annotation.Required;
+import io.fabric8.generator.annotation.ValidationRule;
+import io.fabric8.generator.annotation.ValidationRules;
+import io.fabric8.kubernetes.api.model.apiextensions.v1.ValidationRuleBuilder;
+
+/**
+ * Module that enables injection of attributes from Fabric8 annotations, e.g. {@code @ValidationRule}.
+ */
+public class Fabric8Module implements Module {
+
+ @Override
+ public void applyToConfigBuilder(SchemaGeneratorConfigBuilder builder) {
+ builder.forTypesInGeneral().withTypeAttributeOverride(this::overrideTypeAttributes);
+
+ builder.forFields().withDefaultResolver(this::resolveDefault);
+ builder.forFields().withStringMaxLengthResolver(this::resolveMaxLength);
+ builder.forFields().withStringMinLengthResolver(this::resolveMinLength);
+ builder.forFields().withStringPatternResolver(this::resolvePattern);
+ builder.forFields().withNullableCheck(this::checkNullable);
+ builder.forFields().withRequiredCheck(this::checkRequired);
+ builder.forFields().withInstanceAttributeOverride(this::overrideInstanceAttributes);
+
+ builder.forMethods().withDefaultResolver(this::resolveDefault);
+ builder.forMethods().withStringMaxLengthResolver(this::resolveMaxLength);
+ builder.forMethods().withStringMinLengthResolver(this::resolveMinLength);
+ builder.forMethods().withStringPatternResolver(this::resolvePattern);
+ builder.forMethods().withNullableCheck(this::checkNullable);
+ builder.forMethods().withRequiredCheck(this::checkRequired);
+ builder.forMethods().withInstanceAttributeOverride(this::overrideInstanceAttributes);
+ }
+
+ public Object resolveDefault(MemberScope, ?> member) {
+ return getFromAnnotation(member, Default.class, Default::value).orElse(null);
+ }
+
+ public Integer resolveMaxLength(MemberScope, ?> member) {
+ return getFromAnnotation(member, Max.class, ann -> (int) ann.value()).orElse(null);
+ }
+
+ public Integer resolveMinLength(MemberScope, ?> member) {
+ return getFromAnnotation(member, Min.class, ann -> (int) ann.value()).orElse(null);
+ }
+
+ public String resolvePattern(MemberScope, ?> member) {
+ return getFromAnnotation(member, Pattern.class, Pattern::value).orElse(null);
+ }
+
+ public Boolean checkNullable(MemberScope, ?> member) {
+ return getFromAnnotation(member, Nullable.class, Function.identity()).isPresent();
+ }
+
+ public Boolean checkRequired(MemberScope, ?> member) {
+ return getFromAnnotation(member, Required.class, Function.identity()).isPresent();
+ }
+
+ protected void overrideTypeAttributes(
+ ObjectNode collectedTypeAttributes, TypeScope scope, SchemaGenerationContext context) {
+ // attach CEL validation rules to node
+ ValidationRule[] annotations = scope.getType().getErasedType().getAnnotationsByType(ValidationRule.class);
+ if (annotations.length != 0) {
+ addValidationRules(collectedTypeAttributes, annotations);
+ }
+ }
+
+ protected void overrideInstanceAttributes(
+ ObjectNode memberAttributes, MemberScope, ?> member, SchemaGenerationContext context) {
+ // attach CEL validation rules to node
+ ValidationRule[] annotations = getFromAnnotation(member, ValidationRules.class, ValidationRules::value).orElseGet(() -> {
+ return getFromAnnotation(member, ValidationRule.class, ann -> new ValidationRule[] { ann })
+ .orElse(new ValidationRule[0]);
+ });
+ if (annotations.length != 0) {
+ addValidationRules(memberAttributes, annotations);
+ }
+ }
+
+ protected static Optional getFromAnnotation(
+ MemberScope, ?> member, Class clazz, Function fn) {
+ return Optional.ofNullable(member.getAnnotationConsideringFieldAndGetter(clazz)).map(fn);
+ }
+
+ protected static void addValidationRules(ObjectNode node, ValidationRule... annotations) {
+ List rules = Stream.of(annotations).map(ann -> {
+ return new ValidationRuleBuilder()
+ .withRule(ann.value())
+ .withFieldPath(emptyToNull(ann.fieldPath()))
+ .withMessage(emptyToNull(ann.message()))
+ .withMessageExpression(emptyToNull(ann.messageExpression()))
+ .withOptionalOldSelf(ann.optionalOldSelf() ? true : null)
+ .withReason(emptyToNull(ann.reason()))
+ .build();
+ }).collect(Collectors.toList());
+ node.set("x-kubernetes-validations", convertValue(rules, JsonNode.class));
+ }
+}
\ No newline at end of file
diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/GeneratorUtils.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/GeneratorUtils.java
new file mode 100644
index 00000000000..ee17d9bf078
--- /dev/null
+++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/GeneratorUtils.java
@@ -0,0 +1,45 @@
+package io.fabric8.crdv2.generator.alt;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceColumnDefinition;
+import io.fabric8.kubernetes.client.CustomResource;
+
+public class GeneratorUtils {
+
+ public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ public static T convertValue(Object obj, Class clazz) {
+ return OBJECT_MAPPER.convertValue(obj, clazz);
+ }
+
+ public static String emptyToNull(String value) {
+ return Optional.ofNullable(value).filter(s -> !s.isEmpty()).orElse(null);
+ }
+
+ public static List getPrinterColumns(Class extends CustomResource, ?>> crdClass) {
+ AdditionalPrinterColumn[] annotations = crdClass.getAnnotationsByType(AdditionalPrinterColumn.class);
+ return Stream.of(annotations).map(ann -> {
+ CustomResourceColumnDefinition ret = new CustomResourceColumnDefinition().edit()
+ .withName(ann.name())
+ .withJsonPath(ann.jsonPath())
+ .withType(ann.type().value)
+ .build();
+ if (ann.format() != AdditionalPrinterColumn.Format.NONE) {
+ ret.setFormat(ann.format().value);
+ }
+ if (!ann.description().isEmpty()) {
+ ret.setDescription(ann.description());
+ }
+ if (ann.priority() != 0) {
+ ret.setPriority(ann.priority());
+ }
+ return ret;
+ }).collect(Collectors.toList());
+ }
+}
diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/test/CompatibilityTest.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/test/CompatibilityTest.java
new file mode 100644
index 00000000000..1442c64a677
--- /dev/null
+++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/test/CompatibilityTest.java
@@ -0,0 +1,67 @@
+package io.fabric8.crdv2.generator.alt.test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+
+import io.fabric8.crdv2.example.joke.JokeRequest;
+import io.fabric8.crdv2.generator.CRDGenerator;
+import io.fabric8.crdv2.generator.CRDGenerator.CRDOutput;
+import io.fabric8.crdv2.generator.CustomResourceInfo;
+import io.fabric8.crdv2.generator.alt.ConfigurableCrdGenerator;
+import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceColumnDefinition;
+import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition;
+import io.fabric8.kubernetes.client.CustomResource;
+
+public class CompatibilityTest {
+
+ private static final YAMLMapper YAML_MAPPER = new YAMLMapper();
+
+ private static CustomResourceDefinition standardGenerate(Class extends CustomResource, ?>> crdClass) throws IOException {
+ Map outputs = new HashMap<>();
+ CRDGenerator generator = new CRDGenerator().withOutput(new CRDOutput() {
+
+ @Override
+ public void close() throws IOException {
+ }
+
+ @Override
+ public ByteArrayOutputStream outputFor(String crdName) throws IOException {
+ return outputs.computeIfAbsent(crdName, key -> new ByteArrayOutputStream());
+ }
+ }).forCRDVersions("v1").customResources(CustomResourceInfo.fromClass(crdClass));
+
+ Assertions.assertThat(generator.generate())
+ .isEqualTo(outputs.size())
+ .isEqualTo(1);
+
+ ByteArrayOutputStream out = outputs.values().iterator().next();
+ return YAML_MAPPER.readValue(out.toByteArray(), CustomResourceDefinition.class);
+ }
+
+ private static CustomResourceDefinition generate(Class extends CustomResource> crdClass) {
+ return new ConfigurableCrdGenerator().generateCrd(crdClass);
+ }
+
+ @Test
+ void compareToStandardGenerator() throws IOException {
+ CustomResourceDefinition expected = standardGenerate(JokeRequest.class);
+ CustomResourceDefinition actual = generate(JokeRequest.class);
+ // drop shortNames and additionalPrinterColumns; shortNames is not set by ConfigurableCrdGenerator,
+ // and additionalPrinterColumns is set using different annotation
+ expected.getSpec().getNames().setShortNames(Collections.emptyList());
+ expected.getSpec().getVersions().forEach(v -> v.setAdditionalPrinterColumns(Collections.emptyList()));
+ Assertions.assertThat(actual)
+ .usingRecursiveComparison()
+ .ignoringFieldsOfTypes(CustomResourceColumnDefinition.class)
+ .ignoringCollectionOrder()
+ .isEqualTo(expected);
+ }
+}
diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/test/ExtraFeaturesTest.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/test/ExtraFeaturesTest.java
new file mode 100644
index 00000000000..fc643137a31
--- /dev/null
+++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/test/ExtraFeaturesTest.java
@@ -0,0 +1,79 @@
+package io.fabric8.crdv2.generator.alt.test;
+
+import java.io.IOException;
+
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+
+import io.fabric8.crdv2.generator.alt.AdditionalPrinterColumn;
+import io.fabric8.crdv2.generator.alt.ConfigurableCrdGenerator;
+import io.fabric8.crdv2.generator.alt.EnumDescriptionsModule;
+import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.Version;
+
+public class ExtraFeaturesTest {
+
+ public static final YAMLMapper YAML_MAPPER = new YAMLMapper();
+
+ private ConfigurableCrdGenerator generator;
+
+ public ExtraFeaturesTest() {
+ generator = new ConfigurableCrdGenerator()
+ .register(new EnumDescriptionsModule());
+ }
+
+ @Test
+ public void test() throws IOException {
+ CustomResourceDefinition crd = generator.generateCrd(Resource.class);
+ System.out.println(YAML_MAPPER.writeValueAsString(crd));
+ Assertions.assertThat(crd.getSpec().getVersions()).hasSize(1).element(0)
+ .satisfies(v -> {
+ // check additionalPrinterColumns
+ Assertions.assertThat(v.getAdditionalPrinterColumns()).allSatisfy(col -> {
+ Assertions.assertThat(col)
+ .hasFieldOrPropertyWithValue("jsonPath", ".status.state")
+ .hasFieldOrPropertyWithValue("name", "State");
+ });
+ })
+ .extracting("schema.openAPIV3Schema.properties.status.properties")
+ .hasFieldOrPropertyWithValue("state.description", "The state of the resource\n"
+ + " * `Good` - Indicates the resource is good\n"
+ + " * `NotGood` - Indicates the resource is not good")
+ .extracting("state.enum")
+ .asInstanceOf(InstanceOfAssertFactories.list(TextNode.class))
+ .map(v -> v.asText())
+ .containsExactly("Good", "NotGood");
+ }
+
+ @Group("io.fabric8.test")
+ @Version("v1alpha1")
+ @AdditionalPrinterColumn(jsonPath = ".status.state", name = "State")
+ public static class Resource extends CustomResource {
+
+ private static final long serialVersionUID = -9088417009209691746L;
+ }
+
+ public static class Status {
+
+ @JsonPropertyDescription("The state of the resource")
+ public State state;
+ }
+
+ public static enum State {
+
+ @JsonPropertyDescription("Indicates the resource is good")
+ @JsonProperty("Good")
+ GOOD,
+ @JsonPropertyDescription("Indicates the resource is not good")
+ @JsonProperty("NotGood")
+ NOT_GOOD
+ }
+}
diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/test/JakartaValidationTest.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/test/JakartaValidationTest.java
new file mode 100644
index 00000000000..ce0d8ad9111
--- /dev/null
+++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/test/JakartaValidationTest.java
@@ -0,0 +1,106 @@
+package io.fabric8.crdv2.generator.alt.test;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationModule;
+import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationOption;
+
+import io.fabric8.crdv2.generator.alt.ConfigurableCrdGenerator;
+import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.Version;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+
+public class JakartaValidationTest {
+
+ public static final YAMLMapper YAML_MAPPER = new YAMLMapper();
+
+ private ConfigurableCrdGenerator generator;
+
+ public JakartaValidationTest() {
+ generator = new ConfigurableCrdGenerator()
+ .register(new JakartaValidationModule(
+ JakartaValidationOption.INCLUDE_PATTERN_EXPRESSIONS,
+ JakartaValidationOption.NOT_NULLABLE_FIELD_IS_REQUIRED,
+ JakartaValidationOption.NOT_NULLABLE_METHOD_IS_REQUIRED));
+ }
+
+ @Test
+ public void test() throws IOException {
+ CustomResourceDefinition crd = generator.generateCrd(Resource.class);
+ System.out.println(YAML_MAPPER.writeValueAsString(crd));
+ Assertions.assertThat(crd.getSpec().getVersions()).hasSize(1).element(0)
+ .satisfies(v -> {
+ // check required properties
+ Assertions.assertThat(v)
+ .extracting("schema.openAPIV3Schema.properties.spec.required")
+ .asInstanceOf(InstanceOfAssertFactories.list(String.class))
+ .containsExactlyInAnyOrder("stringAlpha", "stringDigit");
+ })
+ .extracting("schema.openAPIV3Schema.properties.spec.properties")
+ .hasFieldOrPropertyWithValue("stringAlpha.description", "Non-empty string of alphabetical characters")
+ .hasFieldOrPropertyWithValue("stringAlpha.minLength", 1L)
+ .hasFieldOrPropertyWithValue("stringAlpha.pattern", "[A-Za-z]*")
+ .hasFieldOrPropertyWithValue("stringAlpha.type", "string")
+ .hasFieldOrPropertyWithValue("stringDigit.description", "Non-empty list of digits")
+ .hasFieldOrPropertyWithValue("stringDigit.minItems", 1L)
+ .hasFieldOrPropertyWithValue("stringDigit.maxItems", 10L)
+ .hasFieldOrPropertyWithValue("stringDigit.type", "array")
+ .hasFieldOrPropertyWithValue("stringDigit.items.schema.pattern", "[0-9]")
+ .hasFieldOrPropertyWithValue("stringDigit.items.schema.type", "string")
+ .hasFieldOrPropertyWithValue("nestedProps.description", "Optional nested properties")
+ .hasFieldOrPropertyWithValue("nestedProps.properties.floatValue.maximum", 99.0)
+ .hasFieldOrPropertyWithValue("nestedProps.properties.floatValue.type", "number")
+ .hasFieldOrPropertyWithValue("nestedProps.properties.map.minProperties", 3L)
+ .hasFieldOrPropertyWithValue("nestedProps.properties.map.additionalProperties.schema.type", "string")
+ .hasFieldOrPropertyWithValue("nestedProps.properties.map.type", "object")
+ .hasFieldOrPropertyWithValue("nestedProps.type", "object");
+ }
+
+ @Group("io.fabric8.test")
+ @Version("v1alpha1")
+ public static class Resource extends CustomResource {
+
+ private static final long serialVersionUID = -9088417009209691746L;
+ }
+
+ public static class Spec {
+
+ @JsonPropertyDescription("Non-empty string of alphabetical characters")
+ @Pattern(regexp = "[A-Za-z]*")
+ @NotEmpty
+ @NotNull
+ public String stringAlpha;
+
+ @JsonPropertyDescription("Non-empty list of digits")
+ @NotEmpty
+ @Size(max = 10)
+ public List<@Pattern(regexp = "[0-9]") String> stringDigit;
+
+ @JsonPropertyDescription("Optional nested properties")
+ public NestedProperties nestedProps;
+ }
+
+ public static class NestedProperties {
+
+ public Boolean booleanValue;
+ public Integer intValue;
+ @Max(99)
+ public Double floatValue;
+ @Size(min = 3)
+ public Map map;
+ }
+}
diff --git a/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/test/SwaggerTest.java b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/test/SwaggerTest.java
new file mode 100644
index 00000000000..94e97d9d498
--- /dev/null
+++ b/crd-generator/api-v2/src/test/java/io/fabric8/crdv2/generator/alt/test/SwaggerTest.java
@@ -0,0 +1,95 @@
+package io.fabric8.crdv2.generator.alt.test;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import com.github.victools.jsonschema.module.swagger2.Swagger2Module;
+
+import io.fabric8.crdv2.generator.alt.ConfigurableCrdGenerator;
+import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.Version;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.media.Schema.RequiredMode;
+
+public class SwaggerTest {
+
+ public static final YAMLMapper YAML_MAPPER = new YAMLMapper();
+
+ private ConfigurableCrdGenerator generator;
+
+ public SwaggerTest() {
+ generator = new ConfigurableCrdGenerator()
+ .register(new Swagger2Module());
+ }
+
+ @Test
+ public void test() throws IOException {
+ CustomResourceDefinition crd = generator.generateCrd(Resource.class);
+ System.out.println(YAML_MAPPER.writeValueAsString(crd));
+ Assertions.assertThat(crd.getSpec().getVersions()).hasSize(1).element(0)
+ .satisfies(v -> {
+ // check required properties
+ Assertions.assertThat(v)
+ .extracting("schema.openAPIV3Schema.properties.spec.required")
+ .asInstanceOf(InstanceOfAssertFactories.list(String.class))
+ .containsExactlyInAnyOrder("stringAlpha", "stringDigit");
+ })
+ .extracting("schema.openAPIV3Schema.properties.spec.properties")
+ .hasFieldOrPropertyWithValue("stringAlpha.description", "Non-empty string of alphabetical characters")
+ .hasFieldOrPropertyWithValue("stringAlpha.minLength", 1L)
+ .hasFieldOrPropertyWithValue("stringAlpha.pattern", "[A-Za-z]*")
+ .hasFieldOrPropertyWithValue("stringAlpha.type", "string")
+ .hasFieldOrPropertyWithValue("stringDigit.description", "Non-empty list of digits")
+ .hasFieldOrPropertyWithValue("stringDigit.minItems", 1L)
+ .hasFieldOrPropertyWithValue("stringDigit.maxItems", 10L)
+ .hasFieldOrPropertyWithValue("stringDigit.type", "array")
+ .hasFieldOrPropertyWithValue("stringDigit.items.schema.pattern", "[0-9]")
+ .hasFieldOrPropertyWithValue("stringDigit.items.schema.type", "string")
+ .hasFieldOrPropertyWithValue("nestedProps.description", "Optional nested properties")
+ .hasFieldOrPropertyWithValue("nestedProps.properties.floatValue.maximum", 99.0)
+ .hasFieldOrPropertyWithValue("nestedProps.properties.floatValue.type", "number")
+ .hasFieldOrPropertyWithValue("nestedProps.properties.map.minProperties", 3L)
+ .hasFieldOrPropertyWithValue("nestedProps.properties.map.additionalProperties.schema.type", "string")
+ .hasFieldOrPropertyWithValue("nestedProps.properties.map.type", "object")
+ .hasFieldOrPropertyWithValue("nestedProps.type", "object");
+ }
+
+ @Group("io.fabric8.test")
+ @Version("v1alpha1")
+ public static class Resource extends CustomResource {
+
+ private static final long serialVersionUID = -9088417009209691746L;
+ }
+
+ public static class Spec {
+
+ @Schema(description = "Non-empty string of alphabetical characters", pattern = "[A-Za-z]*", minLength = 1, requiredMode = RequiredMode.REQUIRED)
+ public String stringAlpha;
+
+ @Schema(description = "Non-empty list of digits", requiredMode = RequiredMode.REQUIRED)
+ @ArraySchema(schema = @Schema(pattern = "[0-9]"), minItems = 1, maxItems = 10)
+ public List stringDigit;
+
+ @Schema(description = "Optional nested properties")
+ public NestedProperties nestedProps;
+ }
+
+ public static class NestedProperties {
+
+ public Boolean booleanValue;
+ public Integer intValue;
+ @Schema(maximum = "99.0")
+ public Double floatValue;
+ @Schema(minProperties = 3)
+ public Map map;
+ }
+}
diff --git a/pom.xml b/pom.xml
index ddcac5540fb..c7eb9652050 100644
--- a/pom.xml
+++ b/pom.xml
@@ -116,6 +116,9 @@
4.11.0
5.2.0
2.4-M4-groovy-4.0
+ 4.36.0
+ 3.1.0
+ 2.2.22
1.4.2_1
1.11-8_1