diff --git a/schema/json/metaschema-datatypes.json b/schema/json/metaschema-datatypes.json index bed54f6..44d8fea 100644 --- a/schema/json/metaschema-datatypes.json +++ b/schema/json/metaschema-datatypes.json @@ -42,9 +42,8 @@ "pattern": "^-?P([0-9]+D(T(([0-9]+H([0-9]+M)?(([0-9]+|[0-9]+(\\.[0-9]+)?)S)?)|([0-9]+M(([0-9]+|[0-9]+(\\.[0-9]+)?)S)?)|([0-9]+|[0-9]+(\\.[0-9]+)?)S))?)|T(([0-9]+H([0-9]+M)?(([0-9]+|[0-9]+(\\.[0-9]+)?)S)?)|([0-9]+M(([0-9]+|[0-9]+(\\.[0-9]+)?)S)?)|([0-9]+|[0-9]+(\\.[0-9]+)?)S)$" }, "DecimalDatatype": { - "description": "A real number expressed using a whole and optional fractional part separated by a period.", - "type": "number", - "pattern": "^(\\+|-)?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)$" + "description": "A real number expressed using a whole and optional fractional part separated by a period, with optional exponential notation. No leading '+' is allowed.", + "type": "number" }, "EmailAddressDatatype": { "description": "An email address string formatted according to RFC 6531.", diff --git a/schema/xml/metaschema-datatypes.xsd b/schema/xml/metaschema-datatypes.xsd index 601d3c7..b36f17a 100644 --- a/schema/xml/metaschema-datatypes.xsd +++ b/schema/xml/metaschema-datatypes.xsd @@ -68,17 +68,15 @@ - A real number expressed using a whole and optional fractional part - separated by a period. + + A real number expressed using a whole and optional fractional part + separated by a period, with optional exponential notation. + No leading '+' is allowed. Leading zeros are not allowed except + for the integer 0 itself or numbers less than 1 (e.g., 0.5). + - - - - This pattern ensures that leading and trailing whitespace is - disallowed. This helps to even the user experience between implementations - related to whitespace. - - + + diff --git a/test/decimal-type/.gitignore b/test/decimal-type/.gitignore new file mode 100644 index 0000000..aea0a90 --- /dev/null +++ b/test/decimal-type/.gitignore @@ -0,0 +1,5 @@ +# Compiled Java classes +*.class + +# Maven build output +target/ diff --git a/test/decimal-type/README.md b/test/decimal-type/README.md new file mode 100644 index 0000000..cff884d --- /dev/null +++ b/test/decimal-type/README.md @@ -0,0 +1,68 @@ +# Decimal Datatype Test Cases + +This directory contains test schemas and content examples to validate the decimal datatype behavior as defined in PR #135. + +## Schemas + +- `decimal-test.xsd` - XML Schema with inline DecimalDatatype using pattern `-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?` and base type `xs:double` +- `decimal-test.json` - JSON Schema with inline DecimalDatatype using `type: "number"` + +## Expected Behavior Matrix (Tested) + +| Test Case | Example | JSON | XML | +|-----------|---------|:----:|:---:| +| Integers | `12` | ✓ | ✓ | +| Decimals | `12.34` | ✓ | ✓ | +| Positive with leading + | `+12.34` | ✗ | ✗ | +| Exponential notation | `1e3`, `1E+4`, `-2.5e-10` | ✓ | ✓ | +| Leading zeros | `01.23` | ✗ | ✗ | +| No leading digit | `.47` | ✗ | ✗ | +| Trailing decimal point | `123.` | ✗ | ✗ | +| Leading/trailing whitespace | ` 12.34 ` | ✓ | ✓ | + +**Legend:** ✓ = Valid, ✗ = Invalid + +### Notes + +Both JSON and XML ignore/normalize whitespace around numeric values, so leading/trailing whitespace is accepted in both formats. + +## Test Files + +### XML Test Cases + +- `xml-valid-cases.xml` - Contains values that SHOULD pass XML Schema validation +- `xml-invalid-cases.xml` - Contains commented-out invalid cases for individual testing + +### JSON Test Cases + +- `json-valid-cases.json` - Contains values that SHOULD pass JSON Schema validation +- `json-invalid-cases.json` - Documents invalid cases (many cannot be represented in valid JSON syntax) + +## Key Differences Between JSON and XML + +1. **Whitespace Handling**: Both XML Schema and JSON ignore whitespace around numeric values. XML Schema applies whitespace normalization (collapse) for `xs:double` before pattern matching. JSON ignores whitespace around values during parsing. + +2. **Syntax Restrictions**: JSON syntax itself prohibits leading `+`, leading zeros (e.g., `01.23`), `.47`, and `123.` formats, making these parse errors rather than validation errors. The XML pattern explicitly rejects these as well. + +## Running Validation Tests + +### XML Validation + +#### Using xmllint +```bash +xmllint --schema decimal-test.xsd xml-valid-cases.xml --noout +``` + +#### Using the Java XmlValidator (via Maven) +```bash +# Compile the validator +mvn compile + +# Run validation on a specific XML file +mvn exec:java -Dexec.args="decimal-test.xsd xml-valid-cases.xml" +``` + +### JSON Validation (using ajv-cli) +```bash +ajv validate -s decimal-test.json -d json-valid-cases.json +``` diff --git a/test/decimal-type/XmlValidator.java b/test/decimal-type/XmlValidator.java new file mode 100644 index 0000000..3421b2b --- /dev/null +++ b/test/decimal-type/XmlValidator.java @@ -0,0 +1,62 @@ +import javax.xml.XMLConstants; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; +import java.io.File; +import org.xml.sax.SAXNotRecognizedException; +import org.xml.sax.SAXNotSupportedException; + +public class XmlValidator { + public static void main(String[] args) { + if (args.length < 2) { + System.out.println("Usage: java XmlValidator "); + System.exit(1); + } + + String schemaPath = args[0]; + String xmlPath = args[1]; + + File schemaFile = new File(schemaPath); + File xmlFile = new File(xmlPath); + + if (!schemaFile.exists()) { + System.out.println("Schema file not found: " + schemaPath); + System.exit(1); + } + if (!xmlFile.exists()) { + System.out.println("XML file not found: " + xmlPath); + System.exit(1); + } + + try { + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + + // Harden against XXE / SSRF-style external resolution + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + try { + factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + } catch (IllegalArgumentException | SAXNotRecognizedException | SAXNotSupportedException ex) { + // Some JAXP implementations don't support these properties + System.err.println("Warning: could not set external access restrictions: " + ex.getMessage()); + } + + Schema schema = factory.newSchema(schemaFile); + Validator validator = schema.newValidator(); + + try { + validator.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + } catch (IllegalArgumentException ex) { + System.err.println("Warning: could not set validator external access restrictions: " + ex.getMessage()); + } + + validator.validate(new StreamSource(xmlFile)); + System.out.println(xmlPath + " is VALID"); + } catch (Exception e) { + System.err.println(xmlPath + " is INVALID: " + e.getMessage()); + System.exit(1); + } + } +} diff --git a/test/decimal-type/decimal-test.json b/test/decimal-type/decimal-test.json new file mode 100644 index 0000000..e743324 --- /dev/null +++ b/test/decimal-type/decimal-test.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://csrc.nist.gov/ns/metaschema/test/decimal/decimal-test-schema.json", + "$comment": "Schema for testing decimal datatype validation", + "type": "object", + "properties": { + "decimal-tests": { + "type": "object", + "properties": { + "test-cases": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "expected": { + "type": "string", + "enum": ["valid", "invalid"] + }, + "value": { + "description": "A real number expressed using a whole and optional fractional part separated by a period, with optional exponential notation. No leading '+' is allowed.", + "type": "number" + } + }, + "required": ["id", "description", "expected", "value"] + } + } + }, + "required": ["test-cases"] + } + }, + "required": ["decimal-tests"] +} diff --git a/test/decimal-type/decimal-test.xsd b/test/decimal-type/decimal-test.xsd new file mode 100644 index 0000000..8c263b1 --- /dev/null +++ b/test/decimal-type/decimal-test.xsd @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + A real number expressed using a whole and optional fractional part + separated by a period, with optional exponential notation. + No leading '+' is allowed. Leading zeros are not allowed except + for the integer 0 itself or numbers less than 1 (e.g., 0.5). + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/decimal-type/json-invalid-cases.json b/test/decimal-type/json-invalid-cases.json new file mode 100644 index 0000000..9e147d8 --- /dev/null +++ b/test/decimal-type/json-invalid-cases.json @@ -0,0 +1,47 @@ +{ + "$schema": "decimal-test.json", + "_comment": "Invalid decimal test cases for JSON validation. These values SHOULD FAIL JSON Schema validation or are invalid JSON syntax.", + "_note": "JSON does not allow: leading +, leading/trailing whitespace, .47 syntax, 123. syntax. These would be syntax errors or string values.", + "decimal-tests": { + "test-cases": [ + { + "id": "json-placeholder", + "description": "Placeholder - see comments below for invalid cases that cannot be represented in JSON", + "expected": "valid", + "value": 0 + } + ] + }, + "_invalid_cases_documentation": { + "leading_plus": { + "example": "+12.34", + "reason": "JSON syntax does not allow leading + on numbers. This would be a parse error.", + "expected": "invalid" + }, + "leading_zeros": { + "example": "01.23", + "reason": "JSON syntax does not allow leading zeros on numbers (except 0 itself). This would be a parse error.", + "expected": "invalid" + }, + "no_leading_digit": { + "example": ".47", + "reason": "JSON syntax requires a digit before the decimal point. This would be a parse error.", + "expected": "invalid" + }, + "trailing_decimal": { + "example": "123.", + "reason": "JSON syntax requires digits after the decimal point. This would be a parse error.", + "expected": "invalid" + }, + "leading_whitespace": { + "example": " 12.34", + "reason": "If represented as a string \" 12.34\", it would fail type: number validation.", + "expected": "invalid" + }, + "trailing_whitespace": { + "example": "12.34 ", + "reason": "If represented as a string \"12.34 \", it would fail type: number validation.", + "expected": "invalid" + } + } +} diff --git a/test/decimal-type/json-test-string-value.json b/test/decimal-type/json-test-string-value.json new file mode 100644 index 0000000..ec87a93 --- /dev/null +++ b/test/decimal-type/json-test-string-value.json @@ -0,0 +1,13 @@ +{ + "$schema": "decimal-test.json", + "decimal-tests": { + "test-cases": [ + { + "id": "json-string-with-whitespace", + "description": "String value with whitespace should fail type: number", + "expected": "invalid", + "value": " 12.34 " + } + ] + } +} diff --git a/test/decimal-type/json-valid-cases.json b/test/decimal-type/json-valid-cases.json new file mode 100644 index 0000000..0e8264a --- /dev/null +++ b/test/decimal-type/json-valid-cases.json @@ -0,0 +1,68 @@ +{ + "$schema": "decimal-test.json", + "_comment": "Valid decimal test cases for JSON validation. All values SHOULD pass JSON Schema validation with type: number", + "decimal-tests": { + "test-cases": [ + { + "id": "json-int-positive", + "description": "Positive integer (e.g., 12)", + "expected": "valid", + "value": 12 + }, + { + "id": "json-int-negative", + "description": "Negative integer", + "expected": "valid", + "value": -42 + }, + { + "id": "json-int-zero", + "description": "Zero", + "expected": "valid", + "value": 0 + }, + { + "id": "json-dec-positive", + "description": "Positive decimal (e.g., 12.34)", + "expected": "valid", + "value": 12.34 + }, + { + "id": "json-dec-negative", + "description": "Negative decimal", + "expected": "valid", + "value": -12.34 + }, + { + "id": "json-dec-small", + "description": "Small decimal", + "expected": "valid", + "value": 0.001 + }, + { + "id": "json-exp-lower", + "description": "Exponential notation lowercase (e.g., 1e3) - Valid in JSON", + "expected": "valid", + "value": 1e3 + }, + { + "id": "json-exp-upper-plus", + "description": "Exponential notation uppercase with plus (e.g., 1E+4) - Valid in JSON", + "expected": "valid", + "value": 1E+4 + }, + { + "id": "json-exp-negative", + "description": "Exponential notation negative (e.g., -2.5e-10) - Valid in JSON", + "expected": "valid", + "value": -2.5e-10 + }, + { + "id": "json-decimal-no-leading-zero", + "description": "Decimal without unnecessary leading zeros (1.23 is valid; 01.23 is invalid JSON syntax)", + "expected": "valid", + "value": 1.23 + } + ] + } +} diff --git a/test/decimal-type/pom.xml b/test/decimal-type/pom.xml new file mode 100644 index 0000000..28ef2ef --- /dev/null +++ b/test/decimal-type/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + gov.nist.secauto.metaschema.test + decimal-type-test + 1.0.0-SNAPSHOT + jar + + Decimal Type Validation Tests + Test utilities for validating decimal datatype constraints in XML and JSON + + + 11 + 11 + UTF-8 + + + + ${project.basedir} + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + *.java + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + XmlValidator + + + + + diff --git a/test/decimal-type/xml-invalid-cases.xml b/test/decimal-type/xml-invalid-cases.xml new file mode 100644 index 0000000..d1debcf --- /dev/null +++ b/test/decimal-type/xml-invalid-cases.xml @@ -0,0 +1,26 @@ + + + + + + + 0 + + + diff --git a/test/decimal-type/xml-test-leading-plus.xml b/test/decimal-type/xml-test-leading-plus.xml new file mode 100644 index 0000000..e92f445 --- /dev/null +++ b/test/decimal-type/xml-test-leading-plus.xml @@ -0,0 +1,8 @@ + + + + +12.34 + + diff --git a/test/decimal-type/xml-test-leading-whitespace.xml b/test/decimal-type/xml-test-leading-whitespace.xml new file mode 100644 index 0000000..7335f96 --- /dev/null +++ b/test/decimal-type/xml-test-leading-whitespace.xml @@ -0,0 +1,8 @@ + + + + 12.34 + + diff --git a/test/decimal-type/xml-test-leading-zero.xml b/test/decimal-type/xml-test-leading-zero.xml new file mode 100644 index 0000000..0d8b134 --- /dev/null +++ b/test/decimal-type/xml-test-leading-zero.xml @@ -0,0 +1,8 @@ + + + + 01.23 + + diff --git a/test/decimal-type/xml-test-no-leading-digit.xml b/test/decimal-type/xml-test-no-leading-digit.xml new file mode 100644 index 0000000..3a2cbc0 --- /dev/null +++ b/test/decimal-type/xml-test-no-leading-digit.xml @@ -0,0 +1,8 @@ + + + + .47 + + diff --git a/test/decimal-type/xml-test-trailing-decimal.xml b/test/decimal-type/xml-test-trailing-decimal.xml new file mode 100644 index 0000000..9dbfb72 --- /dev/null +++ b/test/decimal-type/xml-test-trailing-decimal.xml @@ -0,0 +1,8 @@ + + + + 123. + + diff --git a/test/decimal-type/xml-valid-cases.xml b/test/decimal-type/xml-valid-cases.xml new file mode 100644 index 0000000..e2d6a1a --- /dev/null +++ b/test/decimal-type/xml-valid-cases.xml @@ -0,0 +1,54 @@ + + + + + + + 12 + + + -42 + + + 0 + + + + + 12.34 + + + -12.34 + + + 0.001 + + + 0.0 + + + + + 1e3 + + + 1E3 + + + 1e+3 + + + 1e-3 + + + -2.5e-10 + + + diff --git a/website/content/specification/datatypes.md b/website/content/specification/datatypes.md index a58388d..0decf63 100644 --- a/website/content/specification/datatypes.md +++ b/website/content/specification/datatypes.md @@ -315,14 +315,14 @@ In JSON Schema, this is represented as: ### decimal -A real number expressed using a whole and optional fractional part separated by a period. +A real number expressed using a whole and optional fractional part separated by a period, with optional exponential notation. No leading `+` is allowed. Leading zeros are not allowed except for the integer `0` itself or numbers less than 1 (e.g., `0.5`). -In XML Schema this is represented as a restriction on the built-in type [decimal](https://www.w3.org/TR/xmlschema11-2/#decimal) as follows: +In XML Schema this is represented as a restriction on the built-in type [double](https://www.w3.org/TR/xmlschema11-2/#double) as follows: ```XML - - + + ``` @@ -331,8 +331,7 @@ In JSON Schema, this is represented as: ```JSON { - "type": "number", - "pattern": "(\\+|-)?([0-9]+(\\.[0-9]*)?|\\.[0-9]+)" + "type": "number" } ``` ### email-address