From e8d39dcb3ed024f6e8b640753be2ed45bceffe5e Mon Sep 17 00:00:00 2001
From: Matt Riben <matt.riben@swirldslabs.com>
Date: Thu, 11 Apr 2024 23:57:51 -0500
Subject: [PATCH] Add crd-generator and java-generator support for Format
 annotation

---
 CHANGELOG.md                                  |  2 +
 .../crd/generator/AbstractJsonSchema.java     | 22 +++++-
 .../fabric8/crd/generator/v1/JsonSchema.java  |  1 +
 .../crd/generator/v1beta1/JsonSchema.java     |  1 +
 .../crd/example/annotated/AnnotatedSpec.java  |  7 ++
 .../crd/generator/v1/JsonSchemaTest.java      | 63 ++++-----------
 doc/CRD-generator.md                          | 24 ++++++
 .../fabric8/generator/annotation/Format.java  | 77 +++++++++++++++++++
 .../nodes/AbstractJSONSchema2Pojo.java        |  7 ++
 .../fabric8/java/generator/nodes/JObject.java |  6 ++
 .../generator/nodes/ValidationProperties.java | 16 +++-
 ...ojos.testAkkaMicroservicesCrd.approved.txt |  1 +
 ...dGeneratePojos.testCrontabCrd.approved.txt |  1 +
 ...estCrontabExtraAnnotationsCrd.approved.txt |  1 +
 14 files changed, 177 insertions(+), 52 deletions(-)
 create mode 100644 generator-annotations/src/main/java/io/fabric8/generator/annotation/Format.java

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 61cb0ae1b83..8d39d6bad51 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,8 @@
 #### Improvements
 * Fix #5878: (java-generator) Add implements Editable for extraAnnotations
 * Fix #5878: (java-generator) Update documentation to include dependencies
+* Fix #5867: (crd-generator) Add support to define `format` from `@Format` annotation
+* Fix #5867: (java-generator) Add support to define `@Format` annotation from `format`
 
 #### Dependency Upgrade
 
diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java
index 68e266807db..ac1672da3ee 100644
--- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java
+++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/AbstractJsonSchema.java
@@ -109,6 +109,7 @@ public abstract class AbstractJsonSchema<T, B> {
   public static final String ANNOTATION_DEFAULT = "io.fabric8.generator.annotation.Default";
   public static final String ANNOTATION_MIN = "io.fabric8.generator.annotation.Min";
   public static final String ANNOTATION_MAX = "io.fabric8.generator.annotation.Max";
+  public static final String ANNOTATION_FORMAT = "io.fabric8.generator.annotation.Format";
   public static final String ANNOTATION_PATTERN = "io.fabric8.generator.annotation.Pattern";
   public static final String ANNOTATION_NULLABLE = "io.fabric8.generator.annotation.Nullable";
   public static final String ANNOTATION_REQUIRED = "io.fabric8.generator.annotation.Required";
@@ -154,6 +155,7 @@ protected static class SchemaPropsOptions {
     final String defaultValue;
     final Double min;
     final Double max;
+    final String format;
     final String pattern;
     final boolean nullable;
     final boolean required;
@@ -164,6 +166,7 @@ protected static class SchemaPropsOptions {
       defaultValue = null;
       min = null;
       max = null;
+      format = null;
       pattern = null;
       nullable = false;
       required = false;
@@ -171,12 +174,13 @@ protected static class SchemaPropsOptions {
       validationRules = null;
     }
 
-    public SchemaPropsOptions(String defaultValue, Double min, Double max, String pattern,
+    public SchemaPropsOptions(String defaultValue, Double min, Double max, String format, String pattern,
         List<KubernetesValidationRule> validationRules,
         boolean nullable, boolean required, boolean preserveUnknownFields) {
       this.defaultValue = defaultValue;
       this.min = min;
       this.max = max;
+      this.format = format;
       this.pattern = pattern;
       this.nullable = nullable;
       this.required = required;
@@ -196,6 +200,10 @@ public Optional<Double> getMax() {
       return Optional.ofNullable(max);
     }
 
+    public Optional<String> getFormat() {
+      return Optional.ofNullable(format);
+    }
+
     public Optional<String> getPattern() {
       return Optional.ofNullable(pattern);
     }
@@ -360,6 +368,7 @@ private T internalFromImpl(TypeDef definition, LinkedHashMap<String, String> vis
           facade.defaultValue,
           facade.min,
           facade.max,
+          facade.format,
           facade.pattern,
           facade.validationRules,
           facade.nullable,
@@ -398,6 +407,7 @@ private static class PropertyOrAccessor {
     private String defaultValue;
     private Double min;
     private Double max;
+    private String format;
     private String pattern;
     private List<KubernetesValidationRule> validationRules;
     private boolean nullable;
@@ -437,6 +447,9 @@ public void process() {
           case ANNOTATION_MIN:
             min = (Double) a.getParameters().get(VALUE);
             break;
+          case ANNOTATION_FORMAT:
+            format = (String) a.getParameters().get(VALUE);
+            break;
           case ANNOTATION_PATTERN:
             pattern = (String) a.getParameters().get(VALUE);
             break;
@@ -494,6 +507,10 @@ public Optional<Double> getMin() {
       return Optional.ofNullable(min);
     }
 
+    public Optional<String> getFormat() {
+      return Optional.ofNullable(format);
+    }
+
     public Optional<String> getPattern() {
       return Optional.ofNullable(pattern);
     }
@@ -547,6 +564,7 @@ private static class PropertyFacade {
     private String defaultValue;
     private Double min;
     private Double max;
+    private String format;
     private String pattern;
     private boolean nullable;
     private boolean required;
@@ -579,6 +597,7 @@ public PropertyFacade(Property property, Map<String, Method> potentialAccessors,
       defaultValue = null;
       min = null;
       max = null;
+      format = null;
       pattern = null;
       validationRules = new LinkedList<>();
     }
@@ -609,6 +628,7 @@ public Property process() {
         defaultValue = p.getDefault().orElse(defaultValue);
         min = p.getMin().orElse(min);
         max = p.getMax().orElse(max);
+        format = p.getFormat().orElse(format);
         pattern = p.getPattern().orElse(pattern);
         p.getValidationRules().ifPresent(rules -> validationRules.addAll(rules));
 
diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/JsonSchema.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/JsonSchema.java
index 03f509df071..c9997f78622 100644
--- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/JsonSchema.java
+++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1/JsonSchema.java
@@ -79,6 +79,7 @@ public void addProperty(Property property, JSONSchemaPropsBuilder builder,
       });
       options.getMin().ifPresent(schema::setMinimum);
       options.getMax().ifPresent(schema::setMaximum);
+      options.getFormat().ifPresent(schema::setFormat);
       options.getPattern().ifPresent(schema::setPattern);
 
       List<ValidationRule> validationRulesFromProperty = options.getValidationRules().stream()
diff --git a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java
index 2cf5e6c048e..aca8917cdb3 100644
--- a/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java
+++ b/crd-generator/api/src/main/java/io/fabric8/crd/generator/v1beta1/JsonSchema.java
@@ -80,6 +80,7 @@ public void addProperty(Property property, JSONSchemaPropsBuilder builder,
       });
       options.getMin().ifPresent(schema::setMinimum);
       options.getMax().ifPresent(schema::setMaximum);
+      options.getFormat().ifPresent(schema::setFormat);
       options.getPattern().ifPresent(schema::setPattern);
 
       List<ValidationRule> validationRulesFromProperty = options.getValidationRules().stream()
diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/example/annotated/AnnotatedSpec.java b/crd-generator/api/src/test/java/io/fabric8/crd/example/annotated/AnnotatedSpec.java
index 40d1b6e6e8f..4a779159d29 100644
--- a/crd-generator/api/src/test/java/io/fabric8/crd/example/annotated/AnnotatedSpec.java
+++ b/crd-generator/api/src/test/java/io/fabric8/crd/example/annotated/AnnotatedSpec.java
@@ -19,6 +19,7 @@
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.annotation.JsonPropertyDescription;
 import io.fabric8.generator.annotation.Default;
+import io.fabric8.generator.annotation.Format;
 import io.fabric8.generator.annotation.Max;
 import io.fabric8.generator.annotation.Min;
 import io.fabric8.generator.annotation.Nullable;
@@ -49,6 +50,7 @@ public class AnnotatedSpec {
   private AnnotatedEnum anEnum;
   @javax.validation.constraints.Min(0) // a non-string value attribute
   private int sizedField;
+  private String password;
 
   @JsonIgnore
   private int ignoredFoo;
@@ -89,6 +91,11 @@ public int getMin() {
     return 1;
   }
 
+  @Format("password")
+  public String getPassword() {
+    return password;
+  }
+
   @Pattern("\\b[1-9]\\b")
   public String getSingleDigit() {
     return "1";
diff --git a/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java b/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java
index a26a1427e7c..fc5fcca721c 100644
--- a/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java
+++ b/crd-generator/api/src/test/java/io/fabric8/crd/generator/v1/JsonSchemaTest.java
@@ -17,6 +17,7 @@
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.TextNode;
 import io.fabric8.crd.example.annotated.Annotated;
 import io.fabric8.crd.example.basic.Basic;
 import io.fabric8.crd.example.extraction.CollectionCyclicSchemaSwap;
@@ -32,6 +33,7 @@
 import io.fabric8.crd.generator.utils.Types;
 import io.fabric8.kubernetes.api.model.AnyType;
 import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaProps;
+import io.fabric8.kubernetes.api.model.apiextensions.v1.JSONSchemaPropsBuilder;
 import io.fabric8.kubernetes.api.model.apiextensions.v1.ValidationRule;
 import io.sundr.model.TypeDef;
 import org.junit.jupiter.api.Test;
@@ -39,9 +41,9 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
-import static io.fabric8.crd.generator.CRDGenerator.YAML_MAPPER;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -103,7 +105,7 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti
     assertNotNull(schema);
     Map<String, JSONSchemaProps> properties = assertSchemaHasNumberOfProperties(schema, 2);
     final JSONSchemaProps specSchema = properties.get("spec");
-    Map<String, JSONSchemaProps> spec = assertSchemaHasNumberOfProperties(specSchema, 15);
+    Map<String, JSONSchemaProps> spec = assertSchemaHasNumberOfProperties(specSchema, 16);
 
     // check descriptions are present
     assertTrue(spec.containsKey("from-field"));
@@ -120,47 +122,16 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti
     assertNull(spec.get("emptySetter").getDescription());
     assertTrue(spec.containsKey("anEnum"));
 
-    final JSONSchemaProps min = spec.get("min");
-    assertNull(min.getDefault());
-    assertEquals(-5.0, min.getMinimum());
-    assertNull(min.getMaximum());
-    assertNull(min.getPattern());
-    assertNull(min.getNullable());
-
-    final JSONSchemaProps max = spec.get("max");
-    assertNull(max.getDefault());
-    assertEquals(5.0, max.getMaximum());
-    assertNull(max.getMinimum());
-    assertNull(max.getPattern());
-    assertNull(max.getNullable());
-
-    final JSONSchemaProps pattern = spec.get("singleDigit");
-    assertNull(pattern.getDefault());
-    assertEquals("\\b[1-9]\\b", pattern.getPattern());
-    assertNull(pattern.getMinimum());
-    assertNull(pattern.getMaximum());
-    assertNull(pattern.getNullable());
-
-    final JSONSchemaProps nullable = spec.get("nullable");
-    assertNull(nullable.getDefault());
-    assertTrue(nullable.getNullable());
-    assertNull(nullable.getMinimum());
-    assertNull(nullable.getMaximum());
-    assertNull(nullable.getPattern());
-
-    final JSONSchemaProps defaultValue = spec.get("defaultValue");
-    assertEquals("my-value", YAML_MAPPER.writeValueAsString(defaultValue.getDefault()).trim());
-    assertNull(defaultValue.getNullable());
-    assertNull(defaultValue.getMinimum());
-    assertNull(defaultValue.getMaximum());
-    assertNull(defaultValue.getPattern());
-
-    final JSONSchemaProps defaultValue2 = spec.get("defaultValue2");
-    assertEquals("my-value2", YAML_MAPPER.writeValueAsString(defaultValue2.getDefault()).trim());
-    assertNull(defaultValue2.getNullable());
-    assertNull(defaultValue2.getMinimum());
-    assertNull(defaultValue2.getMaximum());
-    assertNull(defaultValue2.getPattern());
+    Supplier<JSONSchemaPropsBuilder> typeInteger = () -> new JSONSchemaPropsBuilder().withType("integer");
+    Supplier<JSONSchemaPropsBuilder> typeString = () -> new JSONSchemaPropsBuilder().withType("string");
+    assertEquals(typeInteger.get().withMinimum(-5.0).build(), spec.get("min"));
+    assertEquals(typeInteger.get().withMaximum(5.0).build(), spec.get("max"));
+    assertEquals(typeString.get().withFormat("password").build(), spec.get("password"));
+    assertEquals(typeString.get().withPattern("\\b[1-9]\\b").build(), spec.get("singleDigit"));
+    assertEquals(typeString.get().withNullable(true).build(), spec.get("nullable"));
+    assertEquals(typeString.get().withDefault(TextNode.valueOf("my-value")).build(), spec.get("defaultValue"));
+    assertEquals(typeString.get().withDefault(TextNode.valueOf("my-value2")).build(), spec.get("defaultValue2"));
+    assertEquals(typeString.get().withEnum(TextNode.valueOf("non"), TextNode.valueOf("oui")).build(), spec.get("anEnum"));
 
     // check required list, should register properties with their modified name if needed
     final List<String> required = specSchema.getRequired();
@@ -169,12 +140,6 @@ void shouldAugmentPropertiesSchemaFromAnnotations() throws JsonProcessingExcepti
     assertTrue(required.contains("emptySetter2"));
     assertTrue(required.contains("from-getter"));
 
-    // check the enum values
-    final JSONSchemaProps anEnum = spec.get("anEnum");
-    final List<JsonNode> enumValues = anEnum.getEnum();
-    assertEquals(2, enumValues.size());
-    enumValues.stream().map(JsonNode::textValue).forEach(s -> assertTrue("oui".equals(s) || "non".equals(s)));
-
     // check ignored fields
     assertFalse(spec.containsKey("ignoredFoo"));
     assertFalse(spec.containsKey("ignoredBar"));
diff --git a/doc/CRD-generator.md b/doc/CRD-generator.md
index 036a5466c9c..7865d80fb08 100644
--- a/doc/CRD-generator.md
+++ b/doc/CRD-generator.md
@@ -231,6 +231,30 @@ The field will have the `maximum` property in the generated CRD, such as:
             type: object
 ```
 
+### io.fabric8.generator.annotation.Format
+
+If a field or one of its accessors is annotated with `io.fabric8.generator.annotation.Format`
+
+```java
+public class ExampleSpec {
+  @Format("password")
+  String someValue;
+}
+```
+
+The field will have the `format` property in the generated CRD, such as:
+
+```yaml
+          spec:
+            properties:
+              someValue:
+                format: password
+                type: string
+            required:
+            - someValue
+            type: object
+```
+
 ### io.fabric8.generator.annotation.Pattern
 
 If a field or one of its accessors is annotated with `io.fabric8.generator.annotation.Pattern`
diff --git a/generator-annotations/src/main/java/io/fabric8/generator/annotation/Format.java b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Format.java
new file mode 100644
index 00000000000..ba228e1408d
--- /dev/null
+++ b/generator-annotations/src/main/java/io/fabric8/generator/annotation/Format.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.fabric8.generator.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Java representation of the {@code format} field of JSONSchemaProps.
+ *
+ * <p>
+ * The following formats are validated by Kubernetes:
+ * </p>
+ * <ul>
+ * <li>{@code bsonobjectid}: a bson object ID, i.e. a 24 characters hex string</li>
+ * <li>{@code uri}: an URI as parsed by Golang net/url.ParseRequestURI</li>
+ * <li>{@code email}: an email address as parsed by Golang net/mail.ParseAddress</li>
+ * <li>{@code hostname}: a valid representation for an Internet host name, as defined by RFC 1034, section 3.1 [RFC1034].</li>
+ * <li>{@code ipv4}: an IPv4 IP as parsed by Golang net.ParseIP</li>
+ * <li>{@code ipv6}: an IPv6 IP as parsed by Golang net.ParseIP</li>
+ * <li>{@code cidr}: a CIDR as parsed by Golang net.ParseCIDR</li>
+ * <li>{@code mac}: a MAC address as parsed by Golang net.ParseMAC</li>
+ * <li>{@code uuid}: an UUID that allows uppercase defined by the regex
+ * {@code (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}$}</li>
+ * <li>{@code uuid3}: an UUID3 that allows uppercase defined by the regex
+ * {@code (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?3[0-9a-f]{3}-?[0-9a-f]{4}-?[0-9a-f]{12}$}</li>
+ * <li>{@code uuid4}: an UUID4 that allows uppercase defined by the regex
+ * {@code (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$}</li>
+ * <li>{@code uuid5}: an UUID5 that allows uppercase defined by the regex
+ * {@code (?i)^[0-9a-f]{8}-?[0-9a-f]{4}-?5[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12}$}</li>
+ * <li>{@code isbn}: an ISBN10 or ISBN13 number string like "0321751043" or "978-0321751041"</li>
+ * <li>{@code isbn10}: an ISBN10 number string like "0321751043"</li>
+ * <li>{@code isbn13}: an ISBN13 number string like "978-0321751041"</li>
+ * <li>{@code creditcard}: a credit card number defined by the regex
+ * {@code ^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$}
+ * with any non digit characters mixed in</li>
+ * <li>{@code ssn}: a U.S. social security number following the regex {@code ^\d{3}[- ]?\d{2}[- ]?\d{4}$}</li>
+ * <li>{@code hexcolor}: an hexadecimal color code like "#FFFFFF: following the regex
+ * {@code ^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$}</li>
+ * <li>{@code rgbcolor}: an RGB color code like rgb like "rgb(255,255,2559"</li>
+ * <li>{@code byte}: base64 encoded binary data</li>
+ * <li>{@code password}: any kind of string</li>
+ * <li>{@code date}: a date string like "2006-01-02" as defined by full-date in RFC3339</li>
+ * <li>{@code duration}: a duration string like "22 ns" as parsed by Golang time.ParseDuration or compatible with Scala duration
+ * format</li>
+ * <li>{@code date-time}: a date time string like "2014-12-15T19:30:20.000Z" as defined by date-time in RFC3339.</li>
+ * </ul>
+ * <p>
+ * Unknown formats are ignored by Kubernetes and if another consumer is unaware of the meaning of the format,
+ * they shall fall back to using the basic type without format.
+ * </p>
+ *
+ * @see <a href=
+ *      "https://kubernetes.io/docs/reference/kubernetes-api/extend-resources/custom-resource-definition-v1/#JSONSchemaProps">
+ *      Kubernetes Docs - API Reference - CRD v1 - JSONSchemaProps
+ *      </a>
+ */
+@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Format {
+  String value();
+}
diff --git a/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/AbstractJSONSchema2Pojo.java b/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/AbstractJSONSchema2Pojo.java
index 3cb55d2be5d..7258908e898 100644
--- a/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/AbstractJSONSchema2Pojo.java
+++ b/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/AbstractJSONSchema2Pojo.java
@@ -56,6 +56,7 @@ public static final AnnotationExpr newGeneratedAnnotation() {
 
   protected Double maximum;
   protected Double minimum;
+  protected String format;
   protected String pattern;
 
   public Double getMaximum() {
@@ -66,6 +67,10 @@ public Double getMinimum() {
     return minimum;
   }
 
+  public String getFormat() {
+    return format;
+  }
+
   public String getPattern() {
     return pattern;
   }
@@ -109,6 +114,7 @@ protected AbstractJSONSchema2Pojo(Config config, String description, final boole
     if (validationProperties != null) {
       this.maximum = validationProperties.getMaximum();
       this.minimum = validationProperties.getMinimum();
+      this.format = validationProperties.getFormat();
       this.pattern = validationProperties.getPattern();
     }
   }
@@ -268,6 +274,7 @@ private static AbstractJSONSchema2Pojo fromJsonSchema(
             ValidationProperties.Builder.getInstance()
                 .withMaximum(prop.getMaximum())
                 .withMinimum(prop.getMinimum())
+                .withFormat(prop.getFormat())
                 .withPattern(prop.getPattern())
                 .build());
       case ARRAY:
diff --git a/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/JObject.java b/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/JObject.java
index bc0f034a9c9..1a0c59f23d6 100644
--- a/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/JObject.java
+++ b/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/JObject.java
@@ -248,6 +248,12 @@ public GeneratorResult generateJava() {
                   new Name("io.fabric8.generator.annotation.Min"),
                   new DoubleLiteralExpr(prop.getMinimum())));
         }
+        if (prop.getFormat() != null) {
+          objField.addAnnotation(
+              new SingleMemberAnnotationExpr(
+                  new Name("io.fabric8.generator.annotation.Format"),
+                  new StringLiteralExpr(StringEscapeUtils.escapeJava(prop.getFormat()))));
+        }
         if (prop.getPattern() != null) {
           objField.addAnnotation(
               new SingleMemberAnnotationExpr(
diff --git a/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/ValidationProperties.java b/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/ValidationProperties.java
index 88d8b5b748b..6befa7d42df 100644
--- a/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/ValidationProperties.java
+++ b/java-generator/core/src/main/java/io/fabric8/java/generator/nodes/ValidationProperties.java
@@ -22,11 +22,13 @@
 public class ValidationProperties {
   private final Double maximum;
   private final Double minimum;
+  private final String format;
   private final String pattern;
 
-  private ValidationProperties(final Double maximum, final Double minimum, final String pattern) {
+  private ValidationProperties(final Double maximum, final Double minimum, final String format, final String pattern) {
     this.maximum = maximum;
     this.minimum = minimum;
+    this.format = format;
     this.pattern = pattern;
   }
 
@@ -38,6 +40,10 @@ public Double getMinimum() {
     return minimum;
   }
 
+  public String getFormat() {
+    return format;
+  }
+
   public String getPattern() {
     return pattern;
   }
@@ -45,6 +51,7 @@ public String getPattern() {
   public static final class Builder {
     private Double maximum;
     private Double minimum;
+    private String format;
     private String pattern;
 
     private Builder() {
@@ -64,13 +71,18 @@ public Builder withMinimum(final Double minimum) {
       return this;
     }
 
+    public Builder withFormat(final String format) {
+      this.format = format;
+      return this;
+    }
+
     public Builder withPattern(final String pattern) {
       this.pattern = pattern;
       return this;
     }
 
     public ValidationProperties build() {
-      return new ValidationProperties(maximum, minimum, pattern);
+      return new ValidationProperties(maximum, minimum, format, pattern);
     }
   }
 }
diff --git a/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testAkkaMicroservicesCrd.approved.txt b/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testAkkaMicroservicesCrd.approved.txt
index 4b26bcf4f94..7dd38cfbeeb 100644
--- a/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testAkkaMicroservicesCrd.approved.txt
+++ b/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testAkkaMicroservicesCrd.approved.txt
@@ -549,6 +549,7 @@ public class AkkaMicroserviceStatus implements io.fabric8.kubernetes.api.model.K
      * Total number of available pods targeted by this AkkaMicroservice.
      */
     @com.fasterxml.jackson.annotation.JsonProperty("availableReplicas")
+    @io.fabric8.generator.annotation.Format("int32")
     @com.fasterxml.jackson.annotation.JsonPropertyDescription("Total number of available pods targeted by this AkkaMicroservice.")
     @com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SKIP)
     private Integer availableReplicas;
diff --git a/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabCrd.approved.txt b/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabCrd.approved.txt
index 620fd94a290..401325572eb 100644
--- a/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabCrd.approved.txt
+++ b/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabCrd.approved.txt
@@ -41,6 +41,7 @@ public class CronTabSpec implements io.fabric8.kubernetes.api.model.KubernetesRe
     }
 
     @com.fasterxml.jackson.annotation.JsonProperty("issuedAt")
+    @io.fabric8.generator.annotation.Format("date-time")
     @com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SKIP)
     private java.time.ZonedDateTime issuedAt;
 
diff --git a/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabExtraAnnotationsCrd.approved.txt b/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabExtraAnnotationsCrd.approved.txt
index e1b46e6d5ea..bfa2da2d317 100644
--- a/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabExtraAnnotationsCrd.approved.txt
+++ b/java-generator/core/src/test/resources/io/fabric8/java/generator/approvals/ApprovalTest.generate_withValidCrd_shouldGeneratePojos.testCrontabExtraAnnotationsCrd.approved.txt
@@ -75,6 +75,7 @@ public class CronTabSpec implements io.fabric8.kubernetes.api.builder.Editable<C
     }
 
     @com.fasterxml.jackson.annotation.JsonProperty("issuedAt")
+    @io.fabric8.generator.annotation.Format("date-time")
     @com.fasterxml.jackson.annotation.JsonSetter(nulls = com.fasterxml.jackson.annotation.Nulls.SKIP)
     private java.time.ZonedDateTime issuedAt;