diff --git a/models/ivoa/vo-dml/ivoa_base.vodml-binding.xml b/models/ivoa/vo-dml/ivoa_base.vodml-binding.xml index bf4f8d72..6128cfed 100644 --- a/models/ivoa/vo-dml/ivoa_base.vodml-binding.xml +++ b/models/ivoa/vo-dml/ivoa_base.vodml-binding.xml @@ -7,6 +7,7 @@ org.ivoa.dm http://ivoa.net/vodml/ivoa http://ivoa.net/dm/models/vo-dml/xsd/ivoa + real Double diff --git a/models/sample/test/lifecycleTest.vo-dml.xml b/models/sample/test/lifecycleTest.vo-dml.xml index 265a8f74..30283dab 100644 --- a/models/sample/test/lifecycleTest.vo-dml.xml +++ b/models/sample/test/lifecycleTest.vo-dml.xml @@ -9,10 +9,9 @@ pharriso 0.1 - 2024-11-21T13:44:44Z + 2024-12-19T13:11:08Z - ivoa - 1.0 + null IVOA-v1.0.vo-dml.xml not known @@ -66,18 +65,6 @@ 1 - - Contained.refbad2 - refbad2 - - - lifecycleTest:ReferredLifeCycle - - - 1 - 1 - - ATest diff --git a/models/sample/test/lifecycleTest.vodsl b/models/sample/test/lifecycleTest.vodsl index b63728c9..cd57b31d 100644 --- a/models/sample/test/lifecycleTest.vodsl +++ b/models/sample/test/lifecycleTest.vodsl @@ -16,7 +16,12 @@ otype Contained "" { test2: ivoa:string ""; - refbad2 references ReferredLifeCycle "";/* this is bad because one of the places that Contained could be contained - i.e Atest3 is not in the ReferredLifeCycle containment hierarchy */ +/* below is *really* bad (i.e. breaks the generated code json serialization intention as the + it them moves where the referred to things are apparently contained - as the JSON referencing mechanism + currently in place will output the real object first and then the reference - so it need to come across the "contained" + object first - TODO try to express this restriction in schematron. +*/ +// refbad2 references ReferredLifeCycle "";/* this is bad because one of the places that Contained could be contained - i.e Atest3 is not in the ReferredLifeCycle containment hierarchy */ } otype ATest { //this does things as per current rules diff --git a/runtime/java/build.gradle.kts b/runtime/java/build.gradle.kts index f0ce146d..fc21d3bb 100644 --- a/runtime/java/build.gradle.kts +++ b/runtime/java/build.gradle.kts @@ -16,7 +16,7 @@ dependencies { // implementation("org.glassfish.jaxb:jaxb-runtime:2.3.6") implementation("jakarta.persistence:jakarta.persistence-api:3.1.0") implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0") - implementation("com.networknt:json-schema-validator:1.5.3") + implementation("com.networknt:json-schema-validator:1.5.4") implementation("org.hibernate.orm:hibernate-core:6.5.3.Final") implementation("org.slf4j:slf4j-api:1.7.36") diff --git a/runtime/java/src/main/java/org/ivoa/vodml/ModelDescription.java b/runtime/java/src/main/java/org/ivoa/vodml/ModelDescription.java index 9197b668..faaada5b 100644 --- a/runtime/java/src/main/java/org/ivoa/vodml/ModelDescription.java +++ b/runtime/java/src/main/java/org/ivoa/vodml/ModelDescription.java @@ -38,6 +38,12 @@ public interface ModelDescription { */ String xmlNamespace(); + /** + * The json schema. + * @return the jsonSchema at the head of the model + */ + String jsonSchema(); + /** * Get the classes that make up content. * @return the list of classes. diff --git a/runtime/java/src/main/java/org/ivoa/vodml/validation/AbstractBaseValidation.java b/runtime/java/src/main/java/org/ivoa/vodml/validation/AbstractBaseValidation.java index e7cb07a4..f4e43c6c 100644 --- a/runtime/java/src/main/java/org/ivoa/vodml/validation/AbstractBaseValidation.java +++ b/runtime/java/src/main/java/org/ivoa/vodml/validation/AbstractBaseValidation.java @@ -9,6 +9,7 @@ import java.util.HashMap; import java.util.Map; +import com.networknt.schema.output.OutputUnit; import jakarta.persistence.EntityManager; import jakarta.xml.bind.JAXBContext; import jakarta.xml.bind.JAXBElement; @@ -61,8 +62,13 @@ protected RoundTripResult roundTripJSON(VodmlModel m) throws JsonProc String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(model); System.out.println("JSON output"); System.out.println(json); + JSONValidator jsonValidator = new JSONValidator(m.management()); + OutputUnit vresult = jsonValidator.validate(json); + if (!vresult.isValid()) { + System.err.println(vresult.toString()); + } T retval = mapper.readValue(json, clazz); - return new RoundTripResult(true, retval); + return new RoundTripResult(vresult.isValid(), retval); } diff --git a/runtime/java/src/main/java/org/ivoa/vodml/validation/JSONValidator.java b/runtime/java/src/main/java/org/ivoa/vodml/validation/JSONValidator.java new file mode 100644 index 00000000..bd73eb84 --- /dev/null +++ b/runtime/java/src/main/java/org/ivoa/vodml/validation/JSONValidator.java @@ -0,0 +1,65 @@ +package org.ivoa.vodml.validation; + + +/* + * Created on 17/12/2024 by Paul Harrison (paul.harrison@manchester.ac.uk). + */ + +import com.networknt.schema.*; +import com.networknt.schema.output.OutputUnit; +import org.ivoa.vodml.ModelManagement; + +import java.util.Set; + +/** + * Validate JSON against the generated schema. + */ +public class JSONValidator { + + private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory + .getLogger(JSONValidator.class); + private final ModelManagement modelManagement; + private final SchemaValidatorsConfig config; + private JsonSchemaFactory jsonSchemaFactory; + + /** + * create a validator for a particular model. + * @param modelManagement the model against which the validator should be created. + */ + public JSONValidator(ModelManagement modelManagement) { + + this.modelManagement = modelManagement; + // This creates a schema factory that will use Draft 2020-12 as the default if $schema is not specified +// in the schema data. If $schema is specified in the schema data then that schema dialect will be used +// instead and this version is ignored. + + jsonSchemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012, builder -> + // This creates a mapping from $id which starts with https://www.example.org/ to the retrieval URI classpath: + builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://ivoa.net/dm/", "classpath:")) + ); + + SchemaValidatorsConfig.Builder builder = SchemaValidatorsConfig.builder(); +// By default the JDK regular expression implementation which is not ECMA 262 compliant is used +// Note that setting this requires including optional dependencies +// builder.regularExpressionFactory(GraalJSRegularExpressionFactory.getInstance()); +// builder.regularExpressionFactory(JoniRegularExpressionFactory.getInstance()); + config = builder.build(); + + } + + /** + * validate some JSON against the model schema. + * @param json the input JSON as a string. + * @return the validation result. + */ + public OutputUnit validate(String json) { + JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of(modelManagement.description().jsonSchema()), config); + + OutputUnit ou = schema.validate(json, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionContext -> { + // By default since Draft 2019-09 the format keyword only generates annotations and not assertions + executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); + }); + return ou; + } +} + diff --git a/tools/gradletooling/TODO.md b/tools/gradletooling/TODO.md index 4943d616..98de56c9 100644 --- a/tools/gradletooling/TODO.md +++ b/tools/gradletooling/TODO.md @@ -120,6 +120,7 @@ VODML Tooling TODO * allow refs to be serialized/deserialized as ids always.... - for use in APIs.... https://stackoverflow.com/questions/51172496/how-to-dynamically-ignore-a-property-on-jackson-serialization * perhaps have custom written ivoa base schema.... express some better rules... e.g. non neg integer... * modern usage https://blogs.oracle.com/javamagazine/post/java-json-serialization-jackson + * It might be best to generate schema even for imported models as it is difficult to decide what to do for `"additionalProperties": false` without context of how a particular type is being derived from. # Python production diff --git a/tools/gradletooling/sample/src/test/java/org/ivoa/dm/lifecycle/LifeCycleDetailedTest.java b/tools/gradletooling/sample/src/test/java/org/ivoa/dm/lifecycle/LifeCycleDetailedTest.java index 285d2ad9..6ec27582 100644 --- a/tools/gradletooling/sample/src/test/java/org/ivoa/dm/lifecycle/LifeCycleDetailedTest.java +++ b/tools/gradletooling/sample/src/test/java/org/ivoa/dm/lifecycle/LifeCycleDetailedTest.java @@ -47,7 +47,7 @@ void setUp() throws Exception { List refcont = Arrays.asList(new ReferredLifeCycle("rc1"), new ReferredLifeCycle("rc2")); List contained = - Arrays.asList(new Contained("firstcontained", refcont.get(0)), new Contained("secondContained", refcont.get(1))); + Arrays.asList(new Contained("firstcontained"), new Contained("secondContained")); atest = ATest.createATest( diff --git a/tools/gradletooling/sample/src/test/java/org/ivoa/dm/lifecycle/LifecycleTestModelTest.java b/tools/gradletooling/sample/src/test/java/org/ivoa/dm/lifecycle/LifecycleTestModelTest.java index d33ee2e4..bb6ab1b9 100644 --- a/tools/gradletooling/sample/src/test/java/org/ivoa/dm/lifecycle/LifecycleTestModelTest.java +++ b/tools/gradletooling/sample/src/test/java/org/ivoa/dm/lifecycle/LifecycleTestModelTest.java @@ -59,7 +59,7 @@ public LifecycleTestModel createModel() { List refcont = Arrays.asList(new ReferredLifeCycle("rc1"), new ReferredLifeCycle("rc2")); List contained = - Arrays.asList(new Contained("firstcontained", refcont.get(0)), new Contained("secondContained", refcont.get(1))); + Arrays.asList(new Contained("firstcontained"), new Contained("secondContained")); atest = ATest.createATest( diff --git a/tools/xslt/jaxb.xsl b/tools/xslt/jaxb.xsl index fe12e979..79ffa073 100644 --- a/tools/xslt/jaxb.xsl +++ b/tools/xslt/jaxb.xsl @@ -563,6 +563,11 @@ } + @Override + public String jsonSchema() { + return ""; + } + /** * Return a list of content classes for this model. * @return the list. diff --git a/tools/xslt/vo-dml2jsonschema.xsl b/tools/xslt/vo-dml2jsonschema.xsl index b91630b8..0b72a52a 100644 --- a/tools/xslt/vo-dml2jsonschema.xsl +++ b/tools/xslt/vo-dml2jsonschema.xsl @@ -35,9 +35,11 @@ that allow for successful JSON round tripping. - + + + Generating JSON - considering models @@ -61,7 +63,7 @@ that allow for successful JSON round tripping. } - + ,"additionalProperties": false } } @@ -86,16 +88,19 @@ that allow for successful JSON round tripping. "type" : "object" ,"properties" : { "$comment" : "placeholder to make commas easier!" - + ,"" : { "type": "array" ,"items" : { - + "oneOf" : [ + {}, + {} + ] } } } - + ,"additionalProperties": false } @@ -111,7 +116,7 @@ that allow for successful JSON round tripping. ] } - + ,"additionalProperties": false } @@ -139,7 +144,15 @@ that allow for successful JSON round tripping. - ,"additionalProperties": false + + + ,"additionalProperties" : false + + + ,"unevaluatedProperties" : false + + + @@ -155,9 +168,9 @@ that allow for successful JSON round tripping. , ,"properties" : { "$comment" : "placeholder to make commas easier!" - + ,"@type" : { "type": "string"} - + @@ -186,6 +199,7 @@ that allow for successful JSON round tripping. , ,"properties" : { "$comment" : "placeholder to make commas easier!" + ,"@type" : { "type": "string"} } @@ -239,12 +253,22 @@ that allow for successful JSON round tripping. - + , "" : { , } + + + , "" : { + "type":"array" + ,"items": { + + } + , + } + @@ -265,6 +289,21 @@ that allow for successful JSON round tripping. + + ,"" : { + "type":"object" + + + ,"anyOf": [ + + ] + + + , + + + } + ,"" : { @@ -284,7 +323,20 @@ that allow for successful JSON round tripping. } - + + + , "" : { + "type":"array" + ,"items": { + + } + , + } + + + + , "" : { "oneOf" : [ {}, diff --git a/xsd/vo-dml-binding.xsd b/xsd/vo-dml-binding.xsd index 505523be..fe8d3813 100644 --- a/xsd/vo-dml-binding.xsd +++ b/xsd/vo-dml-binding.xsd @@ -42,12 +42,17 @@ targetNamespace="http://www.ivoa.net/xml/vodml-binding/v0.9.1" attributeFormDefa - + + customization of the xml serialization + + - - whether compositions with multiplicities greater than 1 should have a wrapper element - - + + + whether compositions with multiplicities greater than 1 should have a wrapper element + + + @@ -57,6 +62,21 @@ targetNamespace="http://www.ivoa.net/xml/vodml-binding/v0.9.1" attributeFormDefa + + + customization of the JSON serialization + + + + + whether definitions in the schema model should be "closed off" with "additionalProperties": false - if a type definition is expected to be extended, then this cannot be done + +The default is false which indicates that types should be "closed off". + +In general if a model is expected to be used a a "base model" then this should probably be set to true for the model. + + +