Repository: kestra-io/kestra
Issue: Wrong timezone in Schedule task leads to Scheduler failure and Kestra crashing
Labels: bug
backend

### Explain the bug

[myFile1.log](https://github.com/kestra-io/kestra/files/13614477/myFile1.log)


### Environment Information

- Kestra Version:
- Operating System and Java Version (if not using Kestra Docker image):


Issue comments:
0fa696465b3506cbecd5fd72d8119aad9c6112ca and be383eb6d97161788b51a783f76794873a400b6d prevent the crash, now we need to add a proper validation on flow save

Pull request review comments:

Patch that resolved the issue:
diff --git a/core/src/main/java/io/kestra/core/models/triggers/types/Schedule.java b/core/src/main/java/io/kestra/core/models/triggers/types/Schedule.java
index b6a2795d65..71e061120d 100644
--- a/core/src/main/java/io/kestra/core/models/triggers/types/Schedule.java
+++ b/core/src/main/java/io/kestra/core/models/triggers/types/Schedule.java
@@ -21,7 +21,7 @@
 import io.kestra.core.runners.RunnerUtils;
 import io.kestra.core.services.ConditionService;
 import io.kestra.core.validations.CronExpression;
-import io.swagger.v3.oas.annotations.Hidden;
+import io.kestra.core.validations.TimezoneId;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
 import lombok.experimental.SuperBuilder;
@@ -129,6 +129,7 @@ public class Schedule extends AbstractTrigger implements PollingTriggerInterface
     @PluginProperty
     private String cron;
 
+    @TimezoneId
     @Schema(
         title = "The time zone id to use for evaluating the cron expression. Default value is the server default zone id."
     )
diff --git a/core/src/main/java/io/kestra/core/validations/TimezoneId.java b/core/src/main/java/io/kestra/core/validations/TimezoneId.java
new file mode 100644
index 0000000000..43e6377e2f
--- /dev/null
+++ b/core/src/main/java/io/kestra/core/validations/TimezoneId.java
@@ -0,0 +1,13 @@
+package io.kestra.core.validations;
+
+import io.kestra.core.validations.validator.TimezoneIdValidator;
+
+import javax.validation.Constraint;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = TimezoneIdValidator.class)
+public @interface TimezoneId {
+    String message() default "invalid timezone ({validatedValue})";
+}
\ No newline at end of file
diff --git a/core/src/main/java/io/kestra/core/validations/validator/TimezoneIdValidator.java b/core/src/main/java/io/kestra/core/validations/validator/TimezoneIdValidator.java
new file mode 100644
index 0000000000..c48b2a4ebb
--- /dev/null
+++ b/core/src/main/java/io/kestra/core/validations/validator/TimezoneIdValidator.java
@@ -0,0 +1,36 @@
+package io.kestra.core.validations.validator;
+
+import io.kestra.core.validations.TimezoneId;
+import io.micronaut.core.annotation.AnnotationValue;
+import io.micronaut.core.annotation.Introspected;
+import io.micronaut.core.annotation.NonNull;
+import io.micronaut.core.annotation.Nullable;
+import io.micronaut.validation.validator.constraints.ConstraintValidator;
+import io.micronaut.validation.validator.constraints.ConstraintValidatorContext;
+import jakarta.inject.Singleton;
+
+import java.time.DateTimeException;
+import java.time.ZoneId;
+
+@Singleton
+@Introspected
+public class TimezoneIdValidator implements ConstraintValidator<TimezoneId, String> {
+    @Override
+    public boolean isValid(
+        @Nullable String value,
+        @NonNull AnnotationValue<TimezoneId> annotationMetadata,
+        @NonNull ConstraintValidatorContext context) {
+        if (value == null) {
+            return true;
+        }
+
+        try {
+            ZoneId.of(value);
+        } catch (DateTimeException e) {
+            context.messageTemplate("timezone '({validatedValue})' is not a valid time-zone ID");
+            return false;
+        }
+
+        return true;
+    }
+}


Test patch:
diff --git a/core/src/test/java/io/kestra/core/validations/TimezoneIdTest.java b/core/src/test/java/io/kestra/core/validations/TimezoneIdTest.java
new file mode 100644
index 0000000000..07d76c4d52
--- /dev/null
+++ b/core/src/test/java/io/kestra/core/validations/TimezoneIdTest.java
@@ -0,0 +1,41 @@
+package io.kestra.core.validations;
+
+import io.kestra.core.models.validations.ModelValidator;
+import io.micronaut.core.annotation.Introspected;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+
+@MicronautTest
+class TimezoneIdTest {
+    @Inject
+    private ModelValidator modelValidator;
+
+    @AllArgsConstructor
+    @Introspected
+    @Getter
+    public static class TimezoneIdCls {
+        @TimezoneId
+        String timezone;
+    }
+
+    @Test
+    void inputValidation() {
+        final TimezoneIdCls existingTimezone = new TimezoneIdCls("Europe/Paris");
+
+        assertThat(modelValidator.isValid(existingTimezone).isEmpty(), is(true));
+
+        final TimezoneIdCls invalidTimezone = new TimezoneIdCls("Foo/Bar");
+
+        assertThat(modelValidator.isValid(invalidTimezone).isPresent(), is(true));
+        assertThat(modelValidator.isValid(invalidTimezone).get().getMessage(), allOf(
+            startsWith("timezone"),
+            containsString("is not a valid time-zone ID")
+        ));
+    }
+}