[PECL-CVS] [pecl-file_formats-yaml] yaml12-schema-support: YAML 1.2 schema support

[email protected] (Rasmus Lerdorf) Fri, 10 Apr 2026 20:45:59 +0000
Newsgroups php.pecl.cvs
Message-ID <[email protected]>
Author: Rasmus Lerdorf (rlerdorf)
Date: 2026-04-10T16:45:37-04:00

Commit: https://github.com/php/pecl-file_formats-yaml/commit/8a93c3246f15d94293be027ebd827d8f77ebc528
Raw diff: https://github.com/php/pecl-file_formats-yaml/commit/8a93c3246f15d94293be027ebd827d8f77ebc528.diff

YAML 1.2 schema support

Changed paths:
  A  tests/yaml_parse_schema_12_bool.phpt
  A  tests/yaml_parse_schema_12_constants.phpt
  A  tests/yaml_parse_schema_12_default.phpt
  A  tests/yaml_parse_schema_12_float.phpt
  A  tests/yaml_parse_schema_12_int.phpt
  A  tests/yaml_parse_schema_12_null.phpt
  A  tests/yaml_parse_schema_12_timestamp.phpt
  M  detect.c
  M  parse.c
  M  php_yaml.h
  M  php_yaml_int.h
  M  yaml.c


Diff:

diff --git a/detect.c b/detect.c
index 0a18aba..5af2f72 100644
--- a/detect.c
+++ b/detect.c
@@ -616,6 +616,368 @@ scalar_is_numeric(const char *value, size_t length, zend_long *lval,
 /* }}} */
 
 
+/* {{{ detect_scalar_type_12(const char *, size_t, yaml_event_t)
+ * Guess what datatype the scalar encodes using YAML 1.2 Core Schema
+ */
+const char *detect_scalar_type_12(const char *value, size_t length,
+		const yaml_event_t *event)
+{
+	int flags = 0;
+	zend_long lval = 0;
+	double dval = 0.0;
+
+	/* is value a null? */
+	if (0 == length || scalar_is_null(value, length, event)) {
+		return YAML_NULL_TAG;
+	}
+
+	/* is value numeric? (1.2 rules) */
+	flags = scalar_is_numeric_12(value, length, &lval, &dval, NULL);
+	if (flags != Y_SCALAR_IS_NOT_NUMERIC) {
+		return (flags & Y_SCALAR_IS_FLOAT) ? YAML_FLOAT_TAG : YAML_INT_TAG;
+	}
+
+	/* is value boolean? (1.2 rules: only true/false) */
+	flags = scalar_is_bool_12(value, length, event);
+	if (-1 != flags) {
+		return YAML_BOOL_TAG;
+	}
+
+	/* no implicit timestamp detection in 1.2 Core Schema */
+
+	/* no guess */
+	return NULL;
+}
+/* }}} */
+
+
+/* {{{ scalar_is_bool_12(const char *,size_t,yaml_event_t)
+ * Does this scalar encode a BOOL value under YAML 1.2 Core Schema?
+ *
+ * Only true/True/TRUE and false/False/FALSE are recognized.
+ */
+int
+scalar_is_bool_12(const char *value, size_t length, const yaml_event_t *event)
+{
+	if (NULL == event || IS_NOT_QUOTED_OR_TAG_IS((*event), YAML_BOOL_TAG)) {
+		if (STR_EQ("TRUE", value) ||
+				STR_EQ("True", value) ||
+				STR_EQ("true", value)) {
+			return 1;
+		}
+
+		if (STR_EQ("FALSE", value) ||
+				STR_EQ("False", value) ||
+				STR_EQ("false", value)) {
+			return 0;
+		}
+
+	} else if (NULL != event &&
+			IS_NOT_IMPLICIT_AND_TAG_IS((*event), YAML_BOOL_TAG)) {
+		if (0 == length || (1 == length && '0' == *value)) {
+			return 0;
+		} else {
+			return 1;
+		}
+	}
+
+	return -1;
+}
+/* }}} */
+
+
+/* {{{ scalar_is_numeric_12()
+ * Does this scalar encode a NUMERIC value under YAML 1.2 Core Schema?
+ *
+ * YAML 1.2 Core Schema integers:
+ *   0 | -?[1-9][0-9]* | 0x[0-9a-fA-F]+ | 0o[0-7]+
+ * YAML 1.2 Core Schema floats:
+ *   [-+]?(\.[0-9]+|[0-9]+(\.[0-9]*)?)([eE][-+]?[0-9]+)?
+ *   [-+]?(\.inf|\.Inf|\.INF)
+ *   .nan|.NaN|.NAN
+ *
+ * No binary (0b), no sexagesimal, no underscores, no commas.
+ */
+int
+scalar_is_numeric_12(const char *value, size_t length, zend_long *lval,
+		double *dval, char **str)
+{
+	const char *end = value + length;
+	char *buf = { 0 }, *ptr = { 0 };
+	int negative = 0;
+	int type = 0;
+
+	if (0 == length) {
+		goto not_numeric_12;
+	}
+
+	/* trim */
+	while (value < end && (*(end - 1) == ' ' || *(end - 1) == '\t')) {
+		end--;
+	}
+
+	while (value < end && (*value == ' ' || *value == '\t')) {
+		value++;
+	}
+
+	if (value == end) {
+		goto not_numeric_12;
+	}
+
+	/* not a number */
+	if (STR_EQ(".NAN", value) ||
+			STR_EQ(".NaN", value) ||
+			STR_EQ(".nan", value)) {
+		type = Y_SCALAR_IS_FLOAT | Y_SCALAR_IS_NAN;
+		goto finish_12;
+	}
+
+	/* sign */
+	if (*value == '+') {
+		value++;
+
+	} else if (*value == '-') {
+		negative = 1;
+		value++;
+	}
+
+	if (value == end) {
+		goto not_numeric_12;
+	}
+
+	/* infinity */
+	if (STR_EQ(".INF", value) ||
+			STR_EQ(".Inf", value) ||
+			STR_EQ(".inf", value)) {
+		type = Y_SCALAR_IS_FLOAT;
+		type |= (negative ? Y_SCALAR_IS_INFINITY_N : Y_SCALAR_IS_INFINITY_P);
+		goto finish_12;
+	}
+
+	/* alloc */
+	buf = (char *) emalloc(length + 3);
+	ptr = buf;
+	if (negative) {
+		*ptr++ = '-';
+	}
+
+	/* parse */
+	if (*value == '0') {
+		*ptr++ = *value++;
+		if (value == end) {
+			goto return_zero_12;
+		}
+
+		if (*value == 'x') {
+			/* hexadecimal integer: 0x[0-9a-fA-F]+ */
+			*ptr++ = *value++;
+
+			if (value == end) {
+				goto not_numeric_12;
+			}
+
+			while (value < end) {
+				if ((*value >= '0' && *value <= '9') ||
+						(*value >= 'A' && *value <= 'F') ||
+						(*value >= 'a' && *value <= 'f')) {
+					*ptr++ = *value++;
+
+				} else {
+					goto not_numeric_12;
+				}
+			}
+
+			type = Y_SCALAR_IS_INT | Y_SCALAR_IS_HEXADECIMAL;
+
+		} else if (*value == 'o') {
+			/* octal integer: 0o[0-7]+ */
+			value++;
+
+			if (value == end) {
+				goto not_numeric_12;
+			}
+
+			while (value < end) {
+				if (*value >= '0' && *value <= '7') {
+					*ptr++ = *value++;
+
+				} else {
+					goto not_numeric_12;
+				}
+			}
+
+			type = Y_SCALAR_IS_INT | Y_SCALAR_IS_OCTAL;
+
+		} else if (*value == '.') {
+			goto check_float_12;
+
+		} else {
+			/* bare 0 followed by other digits is not valid in 1.2 */
+			goto not_numeric_12;
+		}
+
+	} else if (*value >= '1' && *value <= '9') {
+		/* decimal integer or float: [1-9][0-9]* */
+		*ptr++ = *value++;
+		while (value < end) {
+			if (*value >= '0' && *value <= '9') {
+				*ptr++ = *value++;
+
+			} else if (*value == '.') {
+				goto check_float_12;
+
+			} else if (*value == 'E' || *value == 'e') {
+				type = Y_SCALAR_IS_FLOAT | Y_SCALAR_IS_DECIMAL;
+				goto check_exp_12;
+
+			} else {
+				goto not_numeric_12;
+			}
+		}
+
+		type = Y_SCALAR_IS_INT | Y_SCALAR_IS_DECIMAL;
+
+	} else if (*value == '.') {
+		/* float starting with dot: .[0-9]+ */
+		*ptr++ = '0';
+
+check_float_12:
+		*ptr++ = *value++;
+		if (value == end) {
+			/* trailing dot with no digits - not a number */
+			goto not_numeric_12;
+		}
+
+		/* need at least one digit, or could be exponent directly */
+		while (value < end) {
+			if (*value >= '0' && *value <= '9') {
+				*ptr++ = *value++;
+
+			} else if (*value == 'E' || *value == 'e') {
+				goto check_exp_12;
+
+			} else {
+				goto not_numeric_12;
+			}
+		}
+
+		type = Y_SCALAR_IS_FLOAT | Y_SCALAR_IS_DECIMAL;
+		goto terminate_12;
+
+check_exp_12:
+		*ptr++ = *value++;
+		if (value == end || (*value != '+' && *value != '-' &&
+				(*value < '0' || *value > '9'))) {
+			goto not_numeric_12;
+		}
+
+		if (*value == '+' || *value == '-') {
+			*ptr++ = *value++;
+		}
+
+		if (value == end || *value < '0' || *value > '9') {
+			goto not_numeric_12;
+		}
+
+		while (value < end) {
+			if (*value >= '0' && *value <= '9') {
+				*ptr++ = *value++;
+
+			} else {
+				goto not_numeric_12;
+			}
+		}
+
+		type = Y_SCALAR_IS_FLOAT | Y_SCALAR_IS_DECIMAL;
+
+	} else {
+		goto not_numeric_12;
+	}
+
+terminate_12:
+	/* terminate */
+	*ptr = '\0';
+
+finish_12:
+	/* convert & assign */
+	if ((type & Y_SCALAR_IS_INT) && lval != NULL) {
+		switch (type & Y_SCALAR_FORMAT_MASK) {
+		case Y_SCALAR_IS_OCTAL:
+			*lval = ZEND_STRTOL(buf, (char **) NULL, 8);
+			break;
+
+		case Y_SCALAR_IS_HEXADECIMAL:
+			*lval = ZEND_STRTOL(buf, (char **) NULL, 16);
+			break;
+
+		default:
+#if PHP_VERSION_ID < 80100
+			ZEND_ATOL(*lval, buf);
+#else
+			*lval = ZEND_ATOL(buf);
+#endif
+			break;
+		}
+
+	} else if ((type & Y_SCALAR_IS_FLOAT) && dval != NULL) {
+		switch (type & Y_SCALAR_FORMAT_MASK) {
+		case Y_SCALAR_IS_INFINITY_P:
+			*dval = php_get_inf();
+			break;
+
+		case Y_SCALAR_IS_INFINITY_N:
+			*dval = -php_get_inf();
+			break;
+
+		case Y_SCALAR_IS_NAN:
+			*dval = php_get_nan();
+			break;
+
+		default:
+			*dval = zend_strtod(buf, (const char **) NULL);
+			break;
+		}
+	}
+
+	if (buf != NULL) {
+		if (str != NULL) {
+			*str = buf;
+
+		} else {
+			efree(buf);
+		}
+	}
+
+	/* return */
+	return type;
+
+
+return_zero_12:
+	if (lval != NULL) {
+		*lval = 0;
+	}
+
+	if (dval != NULL) {
+		*dval = 0.0;
+	}
+
+	if (buf != NULL) {
+		efree(buf);
+	}
+
+	return (Y_SCALAR_IS_INT | Y_SCALAR_IS_ZERO);
+
+
+not_numeric_12:
+	if (buf != NULL) {
+		efree(buf);
+	}
+
+	return Y_SCALAR_IS_NOT_NUMERIC;
+}
+/* }}} */
+
+
 /* {{{ scalar_is_timestamp(const char *,size_t)
  * Does this scalar encode a TIMESTAMP value?
  *
diff --git a/parse.c b/parse.c
index c504cb3..3c7af98 100644
--- a/parse.c
+++ b/parse.c
@@ -702,6 +702,7 @@ void eval_scalar(yaml_event_t event,
 	char *value = (char *) event.data.scalar.value;
 	size_t length = event.data.scalar.length;
 	int flags = 0;
+	int schema_12 = (YAML_G(decode_schema) == Y_SCHEMA_CORE_1_2);
 
 	ZVAL_NULL(retval);
 
@@ -718,7 +719,12 @@ void eval_scalar(yaml_event_t event,
 	}
 
 	/* check for bool */
-	if (-1 != (flags = scalar_is_bool(value, length, &event))) {
+	if (schema_12) {
+		flags = scalar_is_bool_12(value, length, &event);
+	} else {
+		flags = scalar_is_bool(value, length, &event);
+	}
+	if (-1 != flags) {
 		ZVAL_BOOL(retval, (zend_bool) flags);
 		return;
 	}
@@ -731,8 +737,13 @@ void eval_scalar(yaml_event_t event,
 		zend_long lval = 0;
 		double dval = 0.0;
 
-		flags = scalar_is_numeric(
-				value, length, &lval, &dval, NULL);
+		if (schema_12) {
+			flags = scalar_is_numeric_12(
+					value, length, &lval, &dval, NULL);
+		} else {
+			flags = scalar_is_numeric(
+					value, length, &lval, &dval, NULL);
+		}
 		if (flags != Y_SCALAR_IS_NOT_NUMERIC) {
 			if (flags & Y_SCALAR_IS_FLOAT) {
 				ZVAL_DOUBLE(retval, dval);
@@ -767,9 +778,12 @@ void eval_scalar(yaml_event_t event,
 		}
 	}
 
-	/* check for timestamp */
+	/* check for timestamp
+	 * In YAML 1.2 Core Schema, no implicit timestamp detection;
+	 * explicit !!timestamp tags are still honored.
+	 */
 	if (IS_NOT_IMPLICIT_AND_TAG_IS(event, YAML_TIMESTAMP_TAG) ||
-			 scalar_is_timestamp(value, length)) {
+			(!schema_12 && scalar_is_timestamp(value, length))) {
 		if (FAILURE == eval_timestamp(
 				&retval, value, (int) length)) {
 			ZVAL_NULL(retval);
@@ -837,9 +851,15 @@ void eval_scalar_with_callbacks(yaml_event_t event,
 
 	if (YAML_PLAIN_SCALAR_STYLE == event.data.scalar.style && NULL == tag) {
 		/* plain scalar with no specified type */
-		tag = detect_scalar_type(
-				(char *) event.data.scalar.value, event.data.scalar.length,
-				&event);
+		if (YAML_G(decode_schema) == Y_SCHEMA_CORE_1_2) {
+			tag = detect_scalar_type_12(
+					(char *) event.data.scalar.value, event.data.scalar.length,
+					&event);
+		} else {
+			tag = detect_scalar_type(
+					(char *) event.data.scalar.value, event.data.scalar.length,
+					&event);
+		}
 	}
 	if (NULL == tag) {
 		/* couldn't/wouldn't detect tag type, assume string */
diff --git a/php_yaml.h b/php_yaml.h
index eae6793..9550ade 100644
--- a/php_yaml.h
+++ b/php_yaml.h
@@ -85,6 +85,7 @@ ZEND_BEGIN_MODULE_GLOBALS(yaml)
 	zend_bool decode_binary;
 	zend_long decode_timestamp;
 	zend_bool decode_php;
+	zend_long decode_schema;
 	zval *timestamp_decoder;
 	zend_bool output_canonical;
 	zend_long output_indent;
diff --git a/php_yaml_int.h b/php_yaml_int.h
index 6b65220..8c676c2 100644
--- a/php_yaml_int.h
+++ b/php_yaml_int.h
@@ -83,6 +83,10 @@ typedef struct y_emit_state_s {
 #define YAML_PHP_TAG         "!php/object"
 #define YAML_NONSPECIFIC_TAG "!"
 
+#define Y_SCHEMA_DEFAULT   0
+#define Y_SCHEMA_YAML_1_1  1
+#define Y_SCHEMA_CORE_1_2  2
+
 #define Y_SCALAR_IS_NOT_NUMERIC 0x00
 #define Y_SCALAR_IS_INT         0x10
 #define Y_SCALAR_IS_FLOAT       0x20
@@ -136,15 +140,24 @@ void eval_scalar_with_callbacks(
 const char *detect_scalar_type(
 		const char *value, size_t length, const yaml_event_t *event);
 
+const char *detect_scalar_type_12(
+		const char *value, size_t length, const yaml_event_t *event);
+
 int scalar_is_null(
 		const char *value, size_t length, const yaml_event_t *event);
 
 int scalar_is_bool(
 		const char *value, size_t length, const yaml_event_t *event);
 
+int scalar_is_bool_12(
+		const char *value, size_t length, const yaml_event_t *event);
+
 int scalar_is_numeric(
 		const char *value, size_t length, zend_long *lval, double *dval, char **str);
 
+int scalar_is_numeric_12(
+		const char *value, size_t length, zend_long *lval, double *dval, char **str);
+
 int scalar_is_timestamp(const char *value, size_t length);
 
 int php_yaml_write_impl(yaml_emitter_t *emitter, zval *data,
diff --git a/tests/yaml_parse_schema_12_bool.phpt b/tests/yaml_parse_schema_12_bool.phpt
new file mode 100644
index 0000000..31f96d9
--- /dev/null
+++ b/tests/yaml_parse_schema_12_bool.phpt
@@ -0,0 +1,58 @@
+--TEST--
+Yaml 1.2 Core Schema - bool (only true/false recognized)
+--SKIPIF--
+<?php if(!extension_loaded('yaml')) die('skip yaml n/a'); ?>
+--INI--
+yaml.decode_schema=2
+--FILE--
+<?php
+var_dump(yaml_parse('
+# These are booleans in 1.2
+a: true
+b: True
+c: TRUE
+d: false
+e: False
+f: FALSE
+# These are NOT booleans in 1.2 (they were in 1.1)
+g: yes
+h: no
+i: on
+j: off
+k: y
+l: n
+m: Yes
+n: No
+'));
+?>
+--EXPECT--
+array(14) {
+  ["a"]=>
+  bool(true)
+  ["b"]=>
+  bool(true)
+  ["c"]=>
+  bool(true)
+  ["d"]=>
+  bool(false)
+  ["e"]=>
+  bool(false)
+  ["f"]=>
+  bool(false)
+  ["g"]=>
+  string(3) "yes"
+  ["h"]=>
+  string(2) "no"
+  ["i"]=>
+  string(2) "on"
+  ["j"]=>
+  string(3) "off"
+  ["k"]=>
+  string(1) "y"
+  ["l"]=>
+  string(1) "n"
+  ["m"]=>
+  string(3) "Yes"
+  ["n"]=>
+  string(2) "No"
+}
diff --git a/tests/yaml_parse_schema_12_constants.phpt b/tests/yaml_parse_schema_12_constants.phpt
new file mode 100644
index 0000000..b6ebb97
--- /dev/null
+++ b/tests/yaml_parse_schema_12_constants.phpt
@@ -0,0 +1,14 @@
+--TEST--
+Yaml 1.2 Schema - PHP constants defined
+--SKIPIF--
+<?php if(!extension_loaded('yaml')) die('skip yaml n/a'); ?>
+--FILE--
+<?php
+var_dump(YAML_SCHEMA_DEFAULT);
+var_dump(YAML_SCHEMA_YAML_1_1);
+var_dump(YAML_SCHEMA_CORE_1_2);
+?>
+--EXPECT--
+int(0)
+int(1)
+int(2)
diff --git a/tests/yaml_parse_schema_12_default.phpt b/tests/yaml_parse_schema_12_default.phpt
new file mode 100644
index 0000000..7c882cb
--- /dev/null
+++ b/tests/yaml_parse_schema_12_default.phpt
@@ -0,0 +1,24 @@
+--TEST--
+Yaml schema default (0) behaves as 1.1
+--SKIPIF--
+<?php if(!extension_loaded('yaml')) die('skip yaml n/a'); ?>
+--FILE--
+<?php
+// Verify default schema (0) gives YAML 1.1 behavior
+var_dump(ini_get('yaml.decode_schema'));
+var_dump(yaml_parse('
+bool_yes: yes
+bool_no: NO
+octal: 02472256
+'));
+?>
+--EXPECT--
+string(1) "0"
+array(3) {
+  ["bool_yes"]=>
+  bool(true)
+  ["bool_no"]=>
+  bool(false)
+  ["octal"]=>
+  int(685230)
+}
diff --git a/tests/yaml_parse_schema_12_float.phpt b/tests/yaml_parse_schema_12_float.phpt
new file mode 100644
index 0000000..cdf5d76
--- /dev/null
+++ b/tests/yaml_parse_schema_12_float.phpt
@@ -0,0 +1,52 @@
+--TEST--
+Yaml 1.2 Core Schema - float (no sexagesimal, no underscores)
+--SKIPIF--
+<?php if(!extension_loaded('yaml')) die('skip yaml n/a'); ?>
+--INI--
+yaml.decode_schema=2
+--FILE--
+<?php
+var_dump(yaml_parse('
+# Valid 1.2 floats
+canonical: 6.8523015e+5
+fixed: 685230.15
+negative: -1.5
+leading_dot: .25
+inf_lower: .inf
+inf_upper: .INF
+inf_neg: -.inf
+nan_lower: .nan
+nan_upper: .NaN
+nan_all: .NAN
+# NOT floats in 1.2 (they were in 1.1)
+sexagesimal: 190:20:30.15
+underscored: 685_230.15
+'));
+?>
+--EXPECT--
+array(12) {
+  ["canonical"]=>
+  float(685230.15)
+  ["fixed"]=>
+  float(685230.15)
+  ["negative"]=>
+  float(-1.5)
+  ["leading_dot"]=>
+  float(0.25)
+  ["inf_lower"]=>
+  float(INF)
+  ["inf_upper"]=>
+  float(INF)
+  ["inf_neg"]=>
+  float(-INF)
+  ["nan_lower"]=>
+  float(NAN)
+  ["nan_upper"]=>
+  float(NAN)
+  ["nan_all"]=>
+  float(NAN)
+  ["sexagesimal"]=>
+  string(12) "190:20:30.15"
+  ["underscored"]=>
+  string(10) "685_230.15"
+}
diff --git a/tests/yaml_parse_schema_12_int.phpt b/tests/yaml_parse_schema_12_int.phpt
new file mode 100644
index 0000000..579dc55
--- /dev/null
+++ b/tests/yaml_parse_schema_12_int.phpt
@@ -0,0 +1,43 @@
+--TEST--
+Yaml 1.2 Core Schema - int (0o octal, no binary, no sexagesimal, no underscores)
+--SKIPIF--
+<?php if(!extension_loaded('yaml')) die('skip yaml n/a'); ?>
+--INI--
+yaml.decode_schema=2
+--FILE--
+<?php
+var_dump(yaml_parse('
+# Valid 1.2 integers
+decimal: 685230
+negative: -42
+hex: 0xFF
+octal_12: 0o777
+zero: 0
+# NOT integers in 1.2 (they were in 1.1)
+octal_11: 02472256
+binary: 0b1010
+sexagesimal: 190:20:30
+underscored: 1_000_000
+'));
+?>
+--EXPECT--
+array(9) {
+  ["decimal"]=>
+  int(685230)
+  ["negative"]=>
+  int(-42)
+  ["hex"]=>
+  int(255)
+  ["octal_12"]=>
+  int(511)
+  ["zero"]=>
+  int(0)
+  ["octal_11"]=>
+  string(8) "02472256"
+  ["binary"]=>
+  string(6) "0b1010"
+  ["sexagesimal"]=>
+  string(9) "190:20:30"
+  ["underscored"]=>
+  string(9) "1_000_000"
+}
diff --git a/tests/yaml_parse_schema_12_null.phpt b/tests/yaml_parse_schema_12_null.phpt
new file mode 100644
index 0000000..f80b3ef
--- /dev/null
+++ b/tests/yaml_parse_schema_12_null.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Yaml 1.2 Core Schema - null (same as 1.1)
+--SKIPIF--
+<?php if(!extension_loaded('yaml')) die('skip yaml n/a'); ?>
+--INI--
+yaml.decode_schema=2
+--FILE--
+<?php
+var_dump(yaml_parse('
+a: null
+b: Null
+c: NULL
+d: ~
+e:
+'));
+?>
+--EXPECT--
+array(5) {
+  ["a"]=>
+  NULL
+  ["b"]=>
+  NULL
+  ["c"]=>
+  NULL
+  ["d"]=>
+  NULL
+  ["e"]=>
+  NULL
+}
diff --git a/tests/yaml_parse_schema_12_timestamp.phpt b/tests/yaml_parse_schema_12_timestamp.phpt
new file mode 100644
index 0000000..2df1470
--- /dev/null
+++ b/tests/yaml_parse_schema_12_timestamp.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Yaml 1.2 Core Schema - no implicit timestamp detection
+--SKIPIF--
+<?php if(!extension_loaded('yaml')) die('skip yaml n/a'); ?>
+--INI--
+yaml.decode_schema=2
+yaml.decode_timestamp=1
+--FILE--
+<?php
+var_dump(yaml_parse('
+# These are strings in 1.2 Core Schema (no implicit timestamp)
+date: 2024-01-15
+datetime: 2024-01-15T10:30:00Z
+'));
+?>
+--EXPECT--
+array(2) {
+  ["date"]=>
+  string(10) "2024-01-15"
+  ["datetime"]=>
+  string(20) "2024-01-15T10:30:00Z"
+}
diff --git a/yaml.c b/yaml.c
index 7fb0a02..76c6f96 100644
--- a/yaml.c
+++ b/yaml.c
@@ -68,6 +68,8 @@ PHP_INI_BEGIN()
 			decode_timestamp, zend_yaml_globals, yaml_globals)
 	STD_PHP_INI_ENTRY("yaml.decode_php", "0", PHP_INI_ALL, OnUpdateBool,
 			decode_php, zend_yaml_globals, yaml_globals)
+	STD_PHP_INI_ENTRY("yaml.decode_schema", "0", PHP_INI_ALL, OnUpdateLong,
+			decode_schema, zend_yaml_globals, yaml_globals)
 	STD_PHP_INI_ENTRY("yaml.output_canonical", "0", PHP_INI_ALL, OnUpdateBool,
 			output_canonical, zend_yaml_globals, yaml_globals)
 	STD_PHP_INI_ENTRY("yaml.output_indent", "2", PHP_INI_ALL, OnUpdateLong,
@@ -234,6 +236,14 @@ static PHP_MINIT_FUNCTION(yaml)
 	REGISTER_LONG_CONSTANT("YAML_CRLN_BREAK", YAML_CRLN_BREAK,
 			CONST_PERSISTENT | CONST_CS);
 
+	/* schema constants */
+	REGISTER_LONG_CONSTANT("YAML_SCHEMA_DEFAULT", Y_SCHEMA_DEFAULT,
+			CONST_PERSISTENT | CONST_CS);
+	REGISTER_LONG_CONSTANT("YAML_SCHEMA_YAML_1_1", Y_SCHEMA_YAML_1_1,
+			CONST_PERSISTENT | CONST_CS);
+	REGISTER_LONG_CONSTANT("YAML_SCHEMA_CORE_1_2", Y_SCHEMA_CORE_1_2,
+			CONST_PERSISTENT | CONST_CS);
+
 	return SUCCESS;
 }
 /* }}} */
@@ -268,6 +278,7 @@ static PHP_GINIT_FUNCTION(yaml)
 	yaml_globals->decode_binary = 0;
 	yaml_globals->decode_timestamp = 0;
 	yaml_globals->decode_php = 0;
+	yaml_globals->decode_schema = Y_SCHEMA_DEFAULT;
 	yaml_globals->timestamp_decoder = NULL;
 	yaml_globals->output_canonical = 0;
 	yaml_globals->output_indent = 2;