├── .Rbuildignore ├── .gitattributes ├── .github ├── .gitignore └── workflows │ ├── R-CMD-check.yaml │ └── test-coverage.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── DESCRIPTION ├── LICENSE ├── Makefile ├── NAMESPACE ├── NEWS.md ├── R ├── read.R ├── schema.R ├── serialise.R ├── util.R ├── validate.R └── zzz.R ├── README.md ├── docker ├── Dockerfile └── README.md ├── inst ├── LICENSE.ajv ├── LICENSE.is-my-json-valid ├── WORDLIST └── bundle.js ├── js ├── in.js ├── package-lock.json ├── package.json ├── prepare └── webpack.config.js ├── man-roxygen ├── example-json_schema.R ├── example-json_serialise.R ├── example-json_validate.R └── example-json_validator.R ├── man ├── json_schema.Rd ├── json_serialise.Rd ├── json_validate.Rd └── json_validator.Rd ├── tests ├── testthat.R └── testthat │ ├── schema.json │ ├── schema2.json │ ├── test-read.R │ ├── test-serialise.R │ ├── test-util.R │ └── test-validator.R └── vignettes └── jsonvalidate.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^Makefile$ 4 | ^README.Rmd$ 5 | ^.travis.yml$ 6 | ^appveyor.yml$ 7 | ^\.V8history$ 8 | ^appveyor\.yml$ 9 | ^update_web\.sh$ 10 | ^cran-comments\.md$ 11 | ^jsonvalidate_.*\.tar\.gz$ 12 | ^js$ 13 | ^CODE_OF_CONDUCT\.md$ 14 | ^man-roxygen$ 15 | ^doc$ 16 | ^Meta$ 17 | ^\.github$ 18 | ^revdep$ 19 | ^docker$ 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | inst/bundle.js linguist-vendored=true 2 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: R-CMD-check 10 | 11 | jobs: 12 | R-CMD-check: 13 | runs-on: ${{ matrix.config.os }} 14 | 15 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | config: 21 | - {os: macos-latest, r: 'release'} 22 | - {os: windows-latest, r: 'release'} 23 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 24 | - {os: ubuntu-latest, r: 'release'} 25 | - {os: ubuntu-latest, r: 'oldrel-1'} 26 | 27 | env: 28 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 29 | R_KEEP_PKG_SOURCE: yes 30 | 31 | steps: 32 | - uses: actions/checkout@v3 33 | 34 | - uses: r-lib/actions/setup-pandoc@v2 35 | 36 | - uses: r-lib/actions/setup-r@v2 37 | with: 38 | r-version: ${{ matrix.config.r }} 39 | http-user-agent: ${{ matrix.config.http-user-agent }} 40 | use-public-rspm: true 41 | 42 | - uses: r-lib/actions/setup-r-dependencies@v2 43 | with: 44 | extra-packages: any::rcmdcheck 45 | needs: check 46 | 47 | - uses: r-lib/actions/check-r-package@v2 48 | with: 49 | upload-snapshots: true 50 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: test-coverage 10 | 11 | jobs: 12 | test-coverage: 13 | runs-on: ubuntu-latest 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - uses: r-lib/actions/setup-r@v2 21 | with: 22 | use-public-rspm: true 23 | 24 | - uses: r-lib/actions/setup-r-dependencies@v2 25 | with: 26 | extra-packages: any::covr 27 | needs: coverage 28 | 29 | - name: Test coverage 30 | run: | 31 | covr::codecov( 32 | quiet = FALSE, 33 | clean = FALSE, 34 | install_path = file.path(Sys.getenv("RUNNER_TEMP"), "package") 35 | ) 36 | shell: Rscript {0} 37 | 38 | - name: Show testthat output 39 | if: always() 40 | run: | 41 | ## -------------------------------------------------------------------- 42 | find ${{ runner.temp }}/package -name 'testthat.Rout*' -exec cat '{}' \; || true 43 | shell: bash 44 | 45 | - name: Upload test results 46 | if: failure() 47 | uses: actions/upload-artifact@v3 48 | with: 49 | name: coverage-test-failures 50 | path: ${{ runner.temp }}/package 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .V8history 5 | inst/doc 6 | inst/web 7 | *.tar.gz 8 | cran-comments.md 9 | js/node_modules 10 | js/bundle*js 11 | doc 12 | Meta 13 | .idea 14 | revdep 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the Contributor Covenant (http://contributor-covenant.org), version 1.0.0, available at http://contributor-covenant.org/version/1/0/0/ 14 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: jsonvalidate 2 | Title: Validate 'JSON' Schema 3 | Version: 1.5.0 4 | Authors@R: c(person("Rich", "FitzJohn", role = c("aut", "cre"), 5 | email = "rich.fitzjohn@gmail.com"), 6 | person("Rob", "Ashton", role = "aut"), 7 | person("Alex", "Hill", role = "ctb"), 8 | person("Alicia", "Schep", role = "ctb"), 9 | person("Ian", "Lyttle", role = "ctb"), 10 | person("Kara", "Woo", role = "ctb"), 11 | person("Mathias", "Buus", role = c("aut", "cph"), 12 | comment = "Author of bundled imjv library"), 13 | person("Evgeny", "Poberezkin", role = c("aut", "cph"), 14 | comment = "Author of bundled Ajv library")) 15 | Maintainer: Rich FitzJohn 16 | Description: Uses the node library 'is-my-json-valid' or 'ajv' to 17 | validate 'JSON' against a 'JSON' schema. Drafts 04, 06 and 07 of 18 | 'JSON' schema are supported. 19 | License: MIT + file LICENSE 20 | URL: https://docs.ropensci.org/jsonvalidate/, 21 | https://github.com/ropensci/jsonvalidate 22 | BugReports: https://github.com/ropensci/jsonvalidate/issues 23 | Imports: 24 | R6, 25 | V8 26 | Suggests: 27 | knitr, 28 | jsonlite, 29 | rmarkdown, 30 | testthat, 31 | withr 32 | RoxygenNote: 7.3.2 33 | Roxygen: list(markdown = TRUE) 34 | VignetteBuilder: knitr 35 | Encoding: UTF-8 36 | Language: en-GB 37 | Config/testthat/edition: 3 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2024 2 | COPYRIGHT HOLDER: Rich FitzJohn 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGE := $(shell grep '^Package:' DESCRIPTION | sed -E 's/^Package:[[:space:]]+//') 2 | RSCRIPT = Rscript --no-init-file 3 | 4 | all: install 5 | 6 | test: 7 | ${RSCRIPT} -e 'library(methods); devtools::test()' 8 | 9 | roxygen: 10 | @mkdir -p man 11 | ${RSCRIPT} -e "library(methods); devtools::document()" 12 | 13 | install: 14 | R CMD INSTALL . 15 | 16 | build: 17 | R CMD build . 18 | 19 | check: build 20 | _R_CHECK_CRAN_INCOMING_=FALSE R CMD check --as-cran --no-manual `ls -1tr ${PACKAGE}*gz | tail -n1` 21 | @rm -f `ls -1tr ${PACKAGE}*gz | tail -n1` 22 | @rm -rf ${PACKAGE}.Rcheck 23 | 24 | vignettes: vignettes/jsonvalidate.Rmd 25 | ${RSCRIPT} -e 'library(methods); devtools::build_vignettes()' 26 | 27 | staticdocs: 28 | @mkdir -p inst/staticdocs 29 | ${RSCRIPT} -e "library(methods); staticdocs::build_site()" 30 | rm -f vignettes/*.html 31 | @rmdir inst/staticdocs 32 | website: staticdocs 33 | ./update_web.sh 34 | 35 | js/bundle.js: js/package.json js/in.js 36 | ./js/prepare 37 | 38 | inst/bundle.js: js/bundle.js 39 | cp $< $@ 40 | cp js/node_modules/ajv/LICENSE inst/LICENSE.ajv 41 | cp js/node_modules/is-my-json-valid/LICENSE inst/LICENSE.is-my-json-valid 42 | 43 | # No real targets! 44 | .PHONY: all test document install vignettes 45 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(json_schema) 4 | export(json_serialise) 5 | export(json_validate) 6 | export(json_validator) 7 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # jsonvalidate 1.4.1 2 | 3 | * Add support for subfolders in nested schema references. (#61) 4 | 5 | # jsonvalidate 1.4.0 6 | 7 | * Support for safely serialising objects to json, guided by the schema, with new function `json_serialise` 8 | * New object `json_schema` for construction of reusable validation and serialisation functions 9 | 10 | # jsonvalidate 1.3.2 11 | 12 | * Always uses ES5 version of Ajv, which allows use in both current and "legacy" V8 (#51) 13 | 14 | # jsonvalidate 1.3.0 15 | 16 | * Upgrade to ajv version 8.5.0 17 | * Add arg `strict` to `json_validate` and `json_validator` to allow evaluating schema in strict mode for ajv only. This is off (`FALSE`) by default to use permissive behaviour detailed in JSON schema 18 | 19 | # jsonvalidate 1.2.3 20 | 21 | * Schemas can use references to other files with JSON pointers i.e. schemas can reference parts of other files e.g. `definitions.json#/definitions/hello` 22 | * JSON can be validated against a subschema (#18, #19, @AliciaSchep) 23 | * Validation with `error = TRUE` now returns `TRUE` (not `NULL)` on success 24 | * Schemas can span multiple files, being included via `"$ref": "filename.json"` - supported with the ajv engine only (#20, #21, @r-ash). 25 | * Validation can be performed against a fraction of the input data (#25) 26 | 27 | # jsonvalidate 1.1.0 28 | 29 | * Add support for JSON schema draft 06 and 07 using the [`ajv`](https://github.com/ajv-validator/ajv) node library. This must be used by passing the `engine` argument to `json_validate` and `json_validator` at present (#2, #11, #15, #16, #17, @karawoo & @ijlyttle) 30 | 31 | # jsonvalidate 1.0.1 32 | 33 | * Initial CRAN release 34 | -------------------------------------------------------------------------------- /R/read.R: -------------------------------------------------------------------------------- 1 | read_schema <- function(x, v8) { 2 | if (length(x) == 0L) { 3 | stop("zero length input") 4 | } 5 | if (!is.character(x)) { 6 | stop("Expected a character vector") 7 | } 8 | 9 | children <- new.env(parent = emptyenv()) 10 | parent <- NULL 11 | 12 | if (read_schema_is_filename(x)) { 13 | if (!file.exists(x)) { 14 | stop(sprintf("Schema '%s' looks like a filename but does not exist", x)) 15 | } 16 | workdir <- dirname(x) 17 | filename <- basename(x) 18 | ret <- with_dir(workdir, 19 | read_schema_filename(filename, children, parent, v8)) 20 | } else { 21 | ret <- read_schema_string(x, children, parent, v8) 22 | } 23 | 24 | dependencies <- as.list(children) 25 | 26 | ret$meta_schema_version <- check_schema_versions(ret, dependencies) 27 | 28 | if (length(dependencies) > 0L) { 29 | ## It's quite hard to safely ship out the contents of the schema to 30 | ## ajv because it is assuming that we get ready-to-go js. So we 31 | ## need to manually construct safe js here. The alternatives all 32 | ## seem a bit ickier - we could pass in the string representation 33 | ## here and then parse it back out to json (JSON.parse) on each 34 | ## element which would be easier to control but it seems 35 | ## unnecessary. 36 | dependencies <- vcapply(dependencies, function(x) 37 | sprintf('{"id": "%s", "value": %s}', x$filename, x$schema)) 38 | ret$dependencies <- sprintf("[%s]", paste(dependencies, collapse = ", ")) 39 | } 40 | 41 | ret 42 | } 43 | 44 | 45 | read_schema_filename <- function(filename, children, parent, v8) { 46 | ## '$ref' path should be relative to schema ID so if parent is in a 47 | ## subdir we need to add the dir to the filename so it can be sourced 48 | file_path <- filename 49 | if (path_includes_dir(parent[1])) { 50 | file_path <- file.path(dirname(parent[1]), file_path) 51 | } 52 | 53 | if (!file.exists(file_path)) { 54 | additional_msg <- "" 55 | if (file_path != filename) { 56 | additional_msg <- sprintf(" relative to '%s'", parent[1]) 57 | } 58 | stop(sprintf("Did not find schema file '%s'%s", filename, additional_msg)) 59 | } 60 | 61 | schema <- paste(readLines(file_path), collapse = "\n") 62 | 63 | meta_schema_version <- read_meta_schema_version(schema, v8) 64 | read_schema_dependencies(schema, children, c(file_path, parent), v8) 65 | list(schema = schema, filename = file_path, 66 | meta_schema_version = meta_schema_version) 67 | } 68 | 69 | 70 | read_schema_string <- function(string, children, parent, v8) { 71 | meta_schema_version <- read_meta_schema_version(string, v8) 72 | read_schema_dependencies(string, children, c("(string)", parent), v8) 73 | list(schema = string, filename = NULL, 74 | meta_schema_version = meta_schema_version) 75 | } 76 | 77 | 78 | read_schema_dependencies <- function(schema, children, parent, v8) { 79 | extra <- setdiff(find_schema_dependencies(schema, v8), 80 | names(children)) 81 | 82 | ## Remove relative references 83 | extra <- grep("^#", extra, invert = TRUE, value = TRUE) 84 | 85 | if (length(extra) == 0L) { 86 | return(NULL) 87 | } 88 | 89 | if (any(grepl("://", extra))) { 90 | stop("Don't yet support protocol-based sub schemas") 91 | } 92 | 93 | if (any(is_absolute_path(extra))) { 94 | abs <- extra[is_absolute_path(extra)] 95 | abs <- paste0("'", paste(abs, collapse = "', '"), "'") 96 | stop(sprintf("'$ref' paths must be relative, got absolute path(s) %s", abs)) 97 | } 98 | 99 | if (any(grepl("#/", extra))) { 100 | split <- strsplit(extra, "#/") 101 | extra <- lapply(split, "[[", 1) 102 | } 103 | 104 | for (ref in extra) { 105 | ## Mark name as one that we will not descend further with 106 | children[[ref]] <- NULL 107 | ## I feel this should be easier to do with withCallingHandlers, 108 | ## but not getting anywhere there. 109 | children[[ref]] <- tryCatch( 110 | read_schema_filename(ref, children, parent, v8), 111 | error = function(e) { 112 | if (!inherits(e, "jsonvalidate_read_error")) { 113 | chain <- paste(squote(c(rev(parent), ref)), collapse = " > ") 114 | e$message <- sprintf("While reading %s\n%s", chain, e$message) 115 | class(e) <- c("jsonvalidate_read_error", class(e)) 116 | e$call <- NULL 117 | } 118 | stop(e) 119 | }) 120 | } 121 | } 122 | 123 | 124 | read_meta_schema_version <- function(schema, v8) { 125 | meta_schema <- v8$call("get_meta_schema_version", V8::JS(schema)) 126 | if (is.null(meta_schema)) { 127 | return(NULL) 128 | } 129 | 130 | regex <- paste0("^https*://json-schema.org/", 131 | "(draft-\\d{2}|draft/\\d{4}-\\d{2})/schema#*$") 132 | version <- gsub(regex, "\\1", meta_schema) 133 | 134 | version 135 | } 136 | 137 | 138 | find_schema_dependencies <- function(schema, v8) { 139 | v8$call("find_reference", V8::JS(schema)) 140 | } 141 | 142 | 143 | check_schema_versions <- function(schema, dependencies) { 144 | version <- schema$meta_schema_version 145 | 146 | versions <- lapply(dependencies, "[[", "meta_schema_version") 147 | versions <- versions[!vlapply(versions, is.null)] 148 | versions <- vcapply(versions, identity) 149 | version_dependencies <- unique(versions) 150 | 151 | if (length(versions) == 0L) { 152 | return(version) 153 | } 154 | 155 | versions_used <- c(set_names(version, schema$filename %||% "(input string)"), 156 | versions) 157 | versions_used_unique <- unique(versions_used) 158 | if (length(versions_used_unique) == 1L) { 159 | return(versions_used_unique) 160 | } 161 | 162 | err <- split(names(versions_used), versions_used) 163 | err <- vcapply(names(err), function(v) 164 | sprintf(" - %s: %s", v, paste(err[[v]], collapse = ", ")), 165 | USE.NAMES = FALSE) 166 | stop(paste0("Conflicting subschema versions used:\n", 167 | paste(err, collapse = "\n")), 168 | call. = FALSE) 169 | } 170 | 171 | 172 | read_schema_is_filename <- function(x) { 173 | RE_JSON <- "[{['\"]" 174 | !(length(x) != 1 || inherits(x, "AsIs") || grepl(RE_JSON, x)) 175 | } 176 | -------------------------------------------------------------------------------- /R/schema.R: -------------------------------------------------------------------------------- 1 | ##' @name json_schema 2 | ##' @rdname json_schema 3 | ##' @title Interact with JSON schemas 4 | ##' 5 | ##' @description Interact with JSON schemas, using them to validate 6 | ##' json strings or serialise objects to JSON safely. 7 | ##' 8 | ##' This interface supersedes [jsonvalidate::json_schema] and changes 9 | ##' some default arguments. While the old interface is not going 10 | ##' away any time soon, users are encouraged to switch to this 11 | ##' interface, which is what we will develop in the future. 12 | ##' 13 | ##' @example man-roxygen/example-json_serialise.R 14 | NULL 15 | 16 | ## Workaround for https://github.com/r-lib/roxygen2/issues/1158 17 | 18 | ##' @rdname json_schema 19 | ##' @export 20 | json_schema <- R6::R6Class( 21 | "json_schema", 22 | cloneable = FALSE, 23 | 24 | private = list( 25 | v8 = NULL, 26 | do_validate = NULL, 27 | do_serialise = NULL), 28 | 29 | public = list( 30 | ##' @field schema The parsed schema, cannot be rebound 31 | schema = NULL, 32 | 33 | ##' @field engine The name of the schema validation engine 34 | engine = NULL, 35 | 36 | ##' @description Create a new `json_schema` object. 37 | ##' 38 | ##' @param schema Contents of the json schema, or a filename 39 | ##' containing a schema. 40 | ##' 41 | ##' @param engine Specify the validation engine to use. Options are 42 | ##' "ajv" (the default; "Another JSON Schema Validator") or "imjv" 43 | ##' ("is-my-json-valid", the default everywhere in versions prior 44 | ##' to 1.4.0, and the default for [jsonvalidate::json_validator]. 45 | ##' *Use of `ajv` is strongly recommended for all new code*. 46 | ##' 47 | ##' @param reference Reference within schema to use for validating 48 | ##' against a sub-schema instead of the full schema passed in. 49 | ##' For example if the schema has a 'definitions' list including a 50 | ##' definition for a 'Hello' object, one could pass 51 | ##' "#/definitions/Hello" and the validator would check that the json 52 | ##' is a valid "Hello" object. Only available if `engine = "ajv"`. 53 | ##' 54 | ##' @param strict Set whether the schema should be parsed strictly or not. 55 | ##' If in strict mode schemas will error to "prevent any unexpected 56 | ##' behaviours or silently ignored mistakes in user schema". For example 57 | ##' it will error if encounters unknown formats or unknown keywords. See 58 | ##' https://ajv.js.org/strict-mode.html for details. Only available in 59 | ##' `engine = "ajv"` and silently ignored for "imjv". 60 | initialize = function(schema, engine = "ajv", reference = NULL, 61 | strict = FALSE) { 62 | v8 <- jsonvalidate_js() 63 | schema <- read_schema(schema, v8) 64 | if (engine == "imjv") { 65 | private$v8 <- json_schema_imjv(schema, v8, reference) 66 | private$do_validate <- json_validate_imjv 67 | private$do_serialise <- json_serialise_imjv 68 | } else if (engine == "ajv") { 69 | private$v8 <- json_schema_ajv(schema, v8, reference, strict) 70 | private$do_validate <- json_validate_ajv 71 | private$do_serialise <- json_serialise_ajv 72 | } else { 73 | stop(sprintf("Unknown engine '%s'", engine)) 74 | } 75 | 76 | self$engine <- engine 77 | self$schema <- schema 78 | lockBinding("schema", self) 79 | lockBinding("engine", self) 80 | }, 81 | 82 | ##' Validate a json string against a schema. 83 | ##' 84 | ##' @param json Contents of a json object, or a filename containing 85 | ##' one. 86 | ##' 87 | ##' @param verbose Be verbose? If `TRUE`, then an attribute 88 | ##' "errors" will list validation failures as a data.frame 89 | ##' 90 | ##' @param greedy Continue after the first error? 91 | ##' 92 | ##' @param error Throw an error on parse failure? If `TRUE`, 93 | ##' then the function returns `NULL` on success (i.e., call 94 | ##' only for the side-effect of an error on failure, like 95 | ##' `stopifnot`). 96 | ##' 97 | ##' @param query A string indicating a component of the data to 98 | ##' validate the schema against. Eventually this may support full 99 | ##' [jsonpath](https://www.npmjs.com/package/jsonpath) syntax, but 100 | ##' for now this must be the name of an element within `json`. See 101 | ##' the examples for more details. 102 | validate = function(json, verbose = FALSE, greedy = FALSE, error = FALSE, 103 | query = NULL) { 104 | private$do_validate(private$v8, json, verbose, greedy, error, query) 105 | }, 106 | 107 | ##' Serialise an R object to JSON with unboxing guided by the schema. 108 | ##' See [jsonvalidate::json_serialise] for details on the problem and 109 | ##' the algorithm. 110 | ##' 111 | ##' @param object An R object to serialise 112 | serialise = function(object) { 113 | private$do_serialise(private$v8, object) 114 | } 115 | )) 116 | 117 | 118 | json_schema_imjv <- function(schema, v8, reference) { 119 | meta_schema_version <- schema$meta_schema_version %||% "draft-04" 120 | 121 | if (!is.null(reference)) { 122 | ## This one has to be an error; it has never worked and makes no 123 | ## sense. 124 | stop("subschema validation only supported with engine 'ajv'") 125 | } 126 | 127 | if (meta_schema_version != "draft-04") { 128 | ## We detect the version, so let the user know they are not really 129 | ## getting what they're asking for 130 | note_imjv(paste( 131 | "meta schema version other than 'draft-04' is only supported with", 132 | sprintf("engine 'ajv' (requested: '%s')", meta_schema_version), 133 | "- falling back to use 'draft-04'")) 134 | meta_schema_version <- "draft-04" 135 | } 136 | 137 | if (length(schema$dependencies) > 0L) { 138 | ## We've found references, but can't support them. Let the user 139 | ## know. 140 | note_imjv("Schema references are only supported with engine 'ajv'") 141 | } 142 | 143 | v8$call("imjv_create", meta_schema_version, V8::JS(schema$schema)) 144 | 145 | v8 146 | } 147 | 148 | 149 | json_schema_ajv <- function(schema, v8, reference, strict) { 150 | meta_schema_version <- schema$meta_schema_version %||% "draft-07" 151 | 152 | versions_legal <- c("draft-04", "draft-06", "draft-07", "draft/2019-09", 153 | "draft/2020-12") 154 | if (!(meta_schema_version %in% versions_legal)) { 155 | stop(sprintf("Unknown meta schema version '%s'", meta_schema_version)) 156 | } 157 | 158 | if (is.null(reference)) { 159 | reference <- V8::JS("null") 160 | } 161 | if (is.null(schema$filename)) { 162 | schema$filename <- V8::JS("null") 163 | } 164 | dependencies <- V8::JS(schema$dependencies %||% "null") 165 | v8$call("ajv_create", meta_schema_version, strict, 166 | V8::JS(schema$schema), schema$filename, dependencies, reference) 167 | 168 | v8 169 | } 170 | -------------------------------------------------------------------------------- /R/serialise.R: -------------------------------------------------------------------------------- 1 | ##' Safe serialisation of json with unboxing guided by the schema. 2 | ##' 3 | ##' When using [jsonlite::toJSON] we are forced to deal with the 4 | ##' differences between R's types and those available in JSON. In 5 | ##' particular: 6 | ##' 7 | ##' * R has no scalar types so it is not clear if `1` should be 8 | ##' serialised as a number or a vector of length 1; `jsonlite` 9 | ##' provides support for "automatically unboxing" such values 10 | ##' (assuming that length-1 vectors are scalars) or never unboxing 11 | ##' them unless asked to using [jsonlite::unbox] 12 | ##' * JSON has no date/time values and there are many possible string 13 | ##' representations. 14 | ##' * JSON has no [data.frame] or [matrix] type and there are several 15 | ##' ways of representing these in JSON, all equally valid (e.g., row-wise, 16 | ##' column-wise or as an array of objects). 17 | ##' * The handling of `NULL` and missing values (`NA`, `NaN`) are different 18 | ##' * We need to chose the number of digits to write numbers out at, 19 | ##' balancing precision and storage. 20 | ##' 21 | ##' These issues are somewhat lessened when we have a schema because 22 | ##' we know what our target type looks like. This function attempts 23 | ##' to use the schema to guide serialisation of json safely. Currently 24 | ##' it only supports detecting the appropriate treatment of length-1 25 | ##' vectors, but we will expand functionality over time. 26 | ##' 27 | ##' For a user, this function provides an argument-free replacement 28 | ##' for `jsonlite::toJSON`, accepting an R object and returning a 29 | ##' string with the JSON representation of the object. Internally the 30 | ##' algorithm is: 31 | ##' 32 | ##' 1. serialise the object with [jsonlite::toJSON], with 33 | ##' `auto_unbox = FALSE` so that length-1 vectors are serialised as a 34 | ##' length-1 arrays. 35 | ##' 2. operating entirely within JavaScript, deserialise the object 36 | ##' with `JSON.parse`, traverse the object and its schema 37 | ##' simultaneously looking for length-1 arrays where the schema 38 | ##' says there should be scalar value and unboxing these, and 39 | ##' re-serialise with `JSON.stringify` 40 | ##' 41 | ##' There are several limitations to our current approach, and not all 42 | ##' unboxable values will be found - at the moment we know that 43 | ##' schemas contained within a `oneOf` block (or similar) will not be 44 | ##' recursed into. 45 | ##' 46 | ##' # Warning 47 | ##' 48 | ##' Direct use of this function will be slow! If you are going to 49 | ##' serialise more than one or two objects with a single schema, you 50 | ##' should use the `serialise` method of a 51 | ##' [jsonvalidate::json_schema] object which you create once and pass around. 52 | ##' 53 | ##' @title Safe JSON serialisation 54 | ##' 55 | ##' @param object An object to be serialised 56 | ##' 57 | ##' @param schema A schema (string or path to a string, suitable to be 58 | ##' passed through to [jsonvalidate::json_validator] or a validator 59 | ##' object itself. 60 | ##' 61 | ##' @param engine The engine to use. Only ajv is supported, and trying 62 | ##' to use `imjv` will throw an error. 63 | ##' 64 | ##' @inheritParams json_validate 65 | ##' 66 | ##' @return A string, representing `object` in JSON format. As for 67 | ##' `jsonlite::toJSON` we set the class attribute to be `json` to 68 | ##' mark it as serialised json. 69 | ##' 70 | ##' @export 71 | ##' @example man-roxygen/example-json_serialise.R 72 | json_serialise <- function(object, schema, engine = "ajv", reference = NULL, 73 | strict = FALSE) { 74 | obj <- json_schema$new(schema, engine, reference, strict) 75 | obj$serialise(object) 76 | } 77 | 78 | 79 | json_serialise_imjv <- function(v8, object) { 80 | stop("json_serialise is only supported with engine 'ajv'") 81 | } 82 | 83 | 84 | json_serialise_ajv <- function(v8, object) { 85 | str <- jsonlite::toJSON(object, auto_unbox = FALSE) 86 | ret <- v8$call("safeSerialise", str) 87 | class(ret) <- "json" 88 | ret 89 | } 90 | -------------------------------------------------------------------------------- /R/util.R: -------------------------------------------------------------------------------- 1 | get_string <- function(x, what = deparse(substitute(x))) { 2 | if (length(x) == 0L) { 3 | stop(sprintf("zero length input for %s", what)) 4 | } 5 | if (!is.character(x)) { 6 | stop(sprintf("Expected a character vector for %s", what)) 7 | } 8 | 9 | if (refers_to_file(x)) { 10 | x <- paste(readLines(x, warn = FALSE), collapse = "\n") 11 | } else if (length(x) > 1L) { 12 | x <- paste(x, collapse = "\n") 13 | } 14 | 15 | x 16 | } 17 | 18 | 19 | refers_to_file <- function(x) { 20 | ## good reasons not to be a file: 21 | if (length(x) != 1 || inherits(x, "json") || grepl("{", x, fixed = TRUE)) { 22 | return(FALSE) 23 | } 24 | file.exists(x) 25 | } 26 | 27 | 28 | `%||%` <- function(a, b) { 29 | if (is.null(a)) b else a 30 | } 31 | 32 | 33 | with_dir <- function(path, code) { 34 | owd <- setwd(path) 35 | on.exit(setwd(owd)) 36 | force(code) 37 | } 38 | 39 | 40 | vlapply <- function(X, FUN, ...) { 41 | vapply(X, FUN, logical(1), ...) 42 | } 43 | 44 | 45 | vcapply <- function(X, FUN, ...) { 46 | vapply(X, FUN, character(1), ...) 47 | 48 | } 49 | 50 | 51 | squote <- function(x) { 52 | sprintf("'%s'", x) 53 | } 54 | 55 | 56 | set_names <- function(x, nms) { 57 | names(x) <- nms 58 | x 59 | } 60 | 61 | 62 | note_imjv <- function(msg, is_interactive = interactive()) { 63 | ## no_note_imjv interactive => outcome 64 | ## NULL TRUE message 65 | ## NULL FALSE silent 66 | ## FALSE message 67 | ## TRUE silent 68 | no_note_imjv <- getOption("jsonvalidate.no_note_imjv") 69 | 70 | if (is.null(no_note_imjv)) { 71 | show <- is_interactive 72 | } else { 73 | show <- !no_note_imjv 74 | } 75 | if (show) { 76 | message(msg) 77 | } 78 | } 79 | 80 | path_includes_dir <- function(x) { 81 | !is.null(x) && basename(x) != x 82 | } 83 | 84 | is_absolute_path <- function(path) { 85 | grepl("^(/|[A-Za-z]:)", path) 86 | } 87 | -------------------------------------------------------------------------------- /R/validate.R: -------------------------------------------------------------------------------- 1 | ##' Create a validator that can validate multiple json files. 2 | ##' 3 | ##' @section Validation Engines: 4 | ##' 5 | ##' We support two different json validation engines, `imjv` 6 | ##' ("is-my-json-valid") and `ajv` ("Another JSON 7 | ##' Validator"). `imjv` was the original validator included in 8 | ##' the package and remains the default for reasons of backward 9 | ##' compatibility. However, users are encouraged to migrate to 10 | ##' `ajv` as with it we support many more features, including 11 | ##' nested schemas that span multiple files, meta schema versions 12 | ##' later than draft-04, validating using a subschema, and 13 | ##' validating a subset of an input data object. 14 | ##' 15 | ##' If your schema uses these features we will print a message to 16 | ##' screen indicating that you should update when running 17 | ##' interactively. We do not use a warning here as this will be 18 | ##' disruptive to users. You can disable the message by setting the 19 | ##' option `jsonvalidate.no_note_imjv` to `TRUE`. Consider using 20 | ##' [withr::with_options()] (or simply [suppressMessages()]) to 21 | ##' scope this option if you want to quieten it within code you do 22 | ##' not control. Alternatively, setting the option 23 | ##' `jsonvalidate.no_note_imjv` to `FALSE` will print the message 24 | ##' even non-interactively. 25 | ##' 26 | ##' Updating the engine should be simply a case of adding `engine 27 | ##' = "ajv"` to your `json_validator` or `json_validate` 28 | ##' calls, but you may see some issues when doing so. 29 | ##' 30 | ##' * Your json now fails validation: We've seen this where schemas 31 | ##' spanned several files and are silently ignored. By including 32 | ##' these, your data may now fail validation and you will need to 33 | ##' either fix the data or the schema. 34 | ##' 35 | ##' * Your code depended on the exact payload returned by `imjv`: If 36 | ##' you are inspecting the error result and checking numbers of 37 | ##' errors, or even the columns used to describe the errors, you 38 | ##' will likely need to update your code to accommodate the slightly 39 | ##' different format of `ajv` 40 | ##' 41 | ##' * Your schema is simply invalid: If you reference an invalid 42 | ##' metaschema for example, jsonvalidate will fail 43 | ##' 44 | ##' @title Create a json validator 45 | ##' 46 | ##' @param schema Contents of the json schema, or a filename 47 | ##' containing a schema. 48 | ##' 49 | ##' @param engine Specify the validation engine to use. Options are 50 | ##' "imjv" (the default; which uses "is-my-json-valid") and "ajv" 51 | ##' (Another JSON Schema Validator). The latter supports more 52 | ##' recent json schema features. 53 | ##' 54 | ##' @param reference Reference within schema to use for validating against a 55 | ##' sub-schema instead of the full schema passed in. For example 56 | ##' if the schema has a 'definitions' list including a definition for a 57 | ##' 'Hello' object, one could pass "#/definitions/Hello" and the validator 58 | ##' would check that the json is a valid "Hello" object. Only available if 59 | ##' `engine = "ajv"`. 60 | ##' 61 | ##' @param strict Set whether the schema should be parsed strictly or not. 62 | ##' If in strict mode schemas will error to "prevent any unexpected 63 | ##' behaviours or silently ignored mistakes in user schema". For example 64 | ##' it will error if encounters unknown formats or unknown keywords. See 65 | ##' https://ajv.js.org/strict-mode.html for details. Only available in 66 | ##' `engine = "ajv"`. 67 | ##' 68 | ##' @section Using multiple files: 69 | ##' 70 | ##' Multiple files are supported. You can have a schema that references 71 | ##' a file `child.json` using `{"$ref": "child.json"}`---in this case if 72 | ##' `child.json` includes an `id` or `$id` element it will be silently 73 | ##' dropped and the filename used to reference the schema will be used 74 | ##' as the schema id. 75 | ##' 76 | ##' The support is currently quite limited - it will not (yet) read 77 | ##' sub-child schemas relative to child schema `$id` url, and 78 | ##' does not support reading from URLs (only local files are 79 | ##' supported). 80 | ##' 81 | ##' @return A function that can be used to validate a 82 | ##' schema. Additionally, the function has two attributes assigned: 83 | ##' `v8` which is the JavaScript context (used internally) and 84 | ##' `engine`, which contains the name of the engine used. 85 | ##' 86 | ##' @export 87 | ##' @example man-roxygen/example-json_validator.R 88 | json_validator <- function(schema, engine = "imjv", reference = NULL, 89 | strict = FALSE) { 90 | json_schema$new(schema, engine, reference, strict)$validate 91 | } 92 | 93 | 94 | ##' Validate a single json against a schema. This is a convenience 95 | ##' wrapper around `json_validator(schema)(json)` or 96 | ##' `json_schema$new(schema, engine = "ajv")$validate(json)`. See 97 | ##' [jsonvalidate::json_validator()] for further details. 98 | ##' 99 | ##' @title Validate a json file 100 | ##' 101 | ##' @inheritParams json_validator 102 | ##' 103 | ##' @param json Contents of a json object, or a filename containing 104 | ##' one. 105 | ##' 106 | ##' @param verbose Be verbose? If `TRUE`, then an attribute 107 | ##' "errors" will list validation failures as a data.frame 108 | ##' 109 | ##' @param greedy Continue after the first error? 110 | ##' 111 | ##' @param error Throw an error on parse failure? If `TRUE`, 112 | ##' then the function returns `NULL` on success (i.e., call 113 | ##' only for the side-effect of an error on failure, like 114 | ##' `stopifnot`). 115 | ##' 116 | ##' @param query A string indicating a component of the data to 117 | ##' validate the schema against. Eventually this may support full 118 | ##' [jsonpath](https://www.npmjs.com/package/jsonpath) syntax, but 119 | ##' for now this must be the name of an element within `json`. See 120 | ##' the examples for more details. 121 | ##' 122 | ##' @export 123 | ##' @example man-roxygen/example-json_validate.R 124 | json_validate <- function(json, schema, verbose = FALSE, greedy = FALSE, 125 | error = FALSE, engine = "imjv", reference = NULL, 126 | query = NULL, strict = FALSE) { 127 | validator <- json_validator(schema, engine, reference = reference, 128 | strict = strict) 129 | validator(json, verbose, greedy, error, query) 130 | } 131 | 132 | 133 | json_validate_imjv <- function(v8, json, verbose = FALSE, greedy = FALSE, 134 | error = FALSE, query = NULL) { 135 | if (!is.null(query)) { 136 | stop("Queries are only supported with engine 'ajv'") 137 | } 138 | if (error) { 139 | verbose <- TRUE 140 | } 141 | res <- v8$call("imjv_call", V8::JS(get_string(json)), 142 | verbose, greedy) 143 | validation_result(res, error, verbose) 144 | } 145 | 146 | 147 | json_validate_ajv <- function(v8, json, verbose = FALSE, greedy = FALSE, 148 | error = FALSE, query = NULL) { 149 | res <- v8$call("ajv_call", V8::JS(get_string(json)), 150 | error || verbose, query_validate(query)) 151 | validation_result(res, error, verbose) 152 | } 153 | 154 | 155 | validation_result <- function(res, error, verbose) { 156 | success <- res$success 157 | 158 | if (!success) { 159 | if (error) { 160 | stop(validation_error(res)) 161 | } 162 | if (verbose) { 163 | ## In ajv version < 8 errors had dataPath property. This has 164 | ## been renamed to instancePath in v8 +. Keep dataPath for 165 | ## backwards compatibility to support dccvalidator 166 | res$errors$dataPath <- res$errors$instancePath 167 | attr(success, "errors") <- res$errors 168 | } 169 | } 170 | 171 | success 172 | } 173 | 174 | 175 | validation_error <- function(res) { 176 | errors <- res$errors 177 | n <- nrow(errors) 178 | if (res$engine == "ajv") { 179 | detail <- paste(sprintf("\t- %s (%s): %s", 180 | errors$instancePath, 181 | errors$schemaPath, 182 | errors$message), 183 | collapse = "\n") 184 | } else { 185 | detail <- paste(sprintf("\t- %s: %s", 186 | errors[[1]], 187 | errors[[2]]), 188 | collapse = "\n") 189 | } 190 | msg <- sprintf("%s %s validating json:\n%s", 191 | n, ngettext(n, "error", "errors"), detail) 192 | structure( 193 | list(message = msg, errors = errors), 194 | class = c("validation_error", "error", "condition")) 195 | } 196 | 197 | 198 | query_validate <- function(query) { 199 | if (is.null(query)) { 200 | return(V8::JS("null")) 201 | } 202 | ## To ensure backward-compatibility, rule out all but the most 203 | ## simple queries for now 204 | if (grepl("[/@.\\[$\"\'|*?()]", query)) { 205 | stop("Full json-path support is not implemented") 206 | } 207 | query 208 | } 209 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | jsonvalidate_js <- function() { 2 | ct <- V8::v8() 3 | ct$source(system.file("bundle.js", package = "jsonvalidate")) 4 | ct 5 | } 6 | 7 | 8 | ## Via Gabor, remove NOTE about Imports while not loading R6 at load. 9 | ignore_unused_imports <- function() { 10 | R6::R6Class 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonvalidate 2 | 3 | 4 | [![Project Status: Active – The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) 5 | [![R-CMD-check](https://github.com/ropensci/jsonvalidate/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/ropensci/jsonvalidate/actions/workflows/R-CMD-check.yaml) 6 | [![codecov.io](https://codecov.io/github/ropensci/jsonvalidate/coverage.svg?branch=master)](https://app.codecov.io/github/ropensci/jsonvalidate?branch=master) 7 | [![](http://www.r-pkg.org/badges/version/jsonvalidate)](https://cran.r-project.org/package=jsonvalidate) 8 | 9 | 10 | 11 | Validate JSON against a schema using [`is-my-json-valid`](https://github.com/mafintosh/is-my-json-valid) or [`ajv`](https://github.com/ajv-validator/ajv). This package is a thin wrapper around these node libraries, using the [V8](https://cran.r-project.org/package=V8) package. 12 | 13 | ## Usage 14 | 15 | Directly validate `json` against `schema` 16 | 17 | ```r 18 | jsonvalidate::json_validate(json, schema) 19 | ``` 20 | 21 | or create a validator for multiple uses 22 | 23 | ```r 24 | validate <- jsonvalidate::json_validator(schema) 25 | validate(json) 26 | validate(json2) # etc 27 | ``` 28 | 29 | See the [package vignette](https://docs.ropensci.org/jsonvalidate/articles/jsonvalidate.html) for complete examples. 30 | 31 | ## Installation 32 | 33 | Install from CRAN with 34 | 35 | ```r 36 | install.packages("jsonvalidate") 37 | ``` 38 | 39 | Alternatively, the current development version can be installed from GitHub with 40 | 41 | ```r 42 | devtools::install_github("ropensci/jsonvalidate") 43 | ``` 44 | 45 | ## License 46 | 47 | MIT + file LICENSE © [Rich FitzJohn](https://github.com/richfitz). 48 | 49 | Please note that this project is released with a [Contributor Code of Conduct](https://github.com/ropensci/jsonvalidate/blob/master/CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. 50 | 51 | [![ropensci_footer](https://ropensci.org//public_images/github_footer.png)](https://ropensci.org/) 52 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | RUN apt-get update -qq && \ 3 | apt-get install --no-install-recommends -y \ 4 | dirmngr \ 5 | software-properties-common \ 6 | wget && \ 7 | wget -qO- https://cloud.r-project.org/bin/linux/ubuntu/marutter_pubkey.asc | tee -a /etc/apt/trusted.gpg.d/cran_ubuntu_key.asc && \ 8 | add-apt-repository "deb https://cloud.r-project.org/bin/linux/ubuntu $(lsb_release -cs)-cran40/" && \ 9 | DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 10 | build-essential \ 11 | gfortran \ 12 | liblapack-dev \ 13 | libblas-dev \ 14 | libcairo2-dev \ 15 | libcurl4-openssl-dev \ 16 | libfontconfig1-dev \ 17 | libssl-dev \ 18 | libv8-dev \ 19 | libxml2-dev \ 20 | r-base 21 | 22 | RUN apt-get install -y --no-install-recommends locales && \ 23 | echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen && \ 24 | locale-gen en_US.utf8 && \ 25 | /usr/sbin/update-locale LANG=en_US.UTF-8 26 | 27 | ENV LANG=en_US.UTF-8 28 | ENV LC_ALL=en_US.UTF-8 29 | 30 | RUN Rscript -e 'install.packages(c("jsonvalidate", "testthat"))' 31 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # docker/es5 support 2 | 3 | This dockerfile exists to make it easier to test that things still work in an es5 environment, as that is still fairly common (notably Solaris, but also ubuntu 18.04 LTS and RHEL 7). 4 | 5 | From the root directory of jsonvalidate source, run 6 | 7 | ``` 8 | docker build --tag richfitz/jsonvalidate:es5 docker 9 | ``` 10 | 11 | to build the image; this should not take that long. It installs the current r-release along with the CRAN versions of jsonvalidate and testthat (which ensures all core dependencies are present). 12 | 13 | Once setup, you can bring up a container with: 14 | 15 | ``` 16 | docker run --rm -it -v $PWD:/src:ro richfitz/jsonvalidate:es5 bash 17 | ``` 18 | 19 | which mounts the current directory read-only into the container at `/src`. That version of the source (rather than the CRAN one installed in the base image) can be installed with `R CMD INSTALL /src` 20 | 21 | To run the whole test suite run: 22 | 23 | ``` 24 | Rscript -e 'testthat::test_local("/src")' 25 | ``` 26 | 27 | More simply, to just confirm that the bundle is valid you can do 28 | 29 | ``` 30 | Rscript -e 'V8::new_context()$source("/src/inst/bundle.js")' 31 | ``` 32 | 33 | which will error if the bundle is invalid. 34 | 35 | To do a full reverse dependencies check with old libv8, you can bring up R in this container: 36 | 37 | ``` 38 | docker run --rm -it -v $PWD:/src:ro richfitz/jsonvalidate:es5 bash 39 | ``` 40 | 41 | Then install revdepcheck itself 42 | 43 | ``` 44 | install.packages("remotes") 45 | remotes::install_github("r-lib/revdepcheck", upgrade = TRUE) 46 | ``` 47 | 48 | Additional packages that we need, for some reason these did not get installed automatically though we'd have expected them to (see [this issue](https://github.com/r-lib/revdepcheck/issues/209)) 49 | 50 | ``` 51 | install.packages(c( 52 | "cinterpolate", 53 | "deSolve", 54 | "devtools", 55 | "golem", 56 | "inTextSummaryTable", 57 | "patientProfilesVis", 58 | "reticulate", 59 | "rlist", 60 | "shiny", 61 | "shinyBS", 62 | "shinydashboard", 63 | "shinyjs", 64 | "tableschema.r", 65 | "xml2")) 66 | ``` 67 | 68 | At this point you will need to cycle the R session because the package DB will be corrupted by all the installations. 69 | 70 | Finally we can run the reverse dependency check: 71 | 72 | ``` 73 | unlink("/tmp/src", recursive = TRUE) 74 | file.copy("/src", "/tmp", recursive = TRUE) 75 | revdepcheck::revdep_check("/tmp/src", num_workers = 4) 76 | ``` 77 | -------------------------------------------------------------------------------- /inst/LICENSE.ajv: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2021 Evgeny Poberezkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /inst/LICENSE.is-my-json-valid: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /inst/WORDLIST: -------------------------------------------------------------------------------- 1 | CMD 2 | R's 3 | ajv 4 | arg 5 | codecov 6 | deserialise 7 | imjv 8 | io 9 | js 10 | json 11 | jsonpath 12 | metaschema 13 | org 14 | ropensci 15 | subschema 16 | unboxable 17 | -------------------------------------------------------------------------------- /js/in.js: -------------------------------------------------------------------------------- 1 | import "core-js/es/set"; 2 | import "core-js/es/map"; 3 | import "core-js/features/array/find"; 4 | import "core-js/features/array/find-index"; 5 | import "core-js/features/array/from"; 6 | import "core-js/features/array/includes"; 7 | import "core-js/features/object/assign"; 8 | import "core-js/features/string/starts-with"; 9 | import "core-js/features/string/includes"; 10 | 11 | global.Ajv = require('ajv'); 12 | global.AjvSchema4 = require('ajv-draft-04'); 13 | global.AjvSchema6 = require('ajv/dist/refs/json-schema-draft-06.json'); 14 | global.AjvSchema2019 = require('ajv/dist/2019') 15 | global.AjvSchema2020 = require('ajv/dist/2020') 16 | global.addFormats = require('ajv-formats'); 17 | 18 | global.imjv = require('is-my-json-valid'); 19 | 20 | global.ajv_create_object = function(meta_schema_version, strict) { 21 | // Need to disable strict mode, otherwise we get warnings 22 | // about unknown schema entries in draft-04 (e.g., presence of 23 | // const) and draft-07 (e.g. presence of "reference") 24 | var opts = {allErrors: true, 25 | verbose: true, 26 | unicodeRegExp: false, 27 | strict: strict, 28 | code: {es5: true}}; 29 | if (meta_schema_version === "draft-04") { 30 | // Need to drop keywords present in later schema versions, 31 | // otherwise they seem to be not ignored (e.g., a schema that 32 | // has the 'const' keyword will check it, even though that 33 | // keyword is not part of draft-04) 34 | var ret = new AjvSchema4(opts) 35 | .removeKeyword('propertyNames') 36 | .removeKeyword('contains') 37 | .removeKeyword('const') 38 | .removeKeyword('if') 39 | .removeKeyword('then') 40 | .removeKeyword('else'); 41 | } else if (meta_schema_version === "draft/2019-09") { 42 | var ret = new AjvSchema2019(opts); 43 | } else if (meta_schema_version === "draft/2020-12") { 44 | var ret = new AjvSchema2020(opts); 45 | } else { 46 | var ret = new Ajv(opts); 47 | if (meta_schema_version === "draft-06") { 48 | ret.addMetaSchema(AjvSchema6); 49 | } 50 | } 51 | addFormats(ret); 52 | return ret; 53 | } 54 | 55 | // TODO: we can push greedy into here 56 | global.ajv_create = function(meta_schema_version, strict, schema, 57 | filename, dependencies, reference) { 58 | var ret = ajv_create_object(meta_schema_version, strict); 59 | 60 | if (dependencies) { 61 | dependencies.forEach( 62 | function(x) { 63 | // Avoid adding a dependency and then adding it again as the 64 | // main schema. This might occur if we have recusive references. 65 | if (x.id !== filename) { 66 | ret.addSchema(drop_id(x.value), x.id) 67 | } 68 | }); 69 | } 70 | 71 | if (reference === null) { 72 | ret = ret.addSchema(drop_id(schema), filename).getSchema(filename); 73 | } else { 74 | ret = ret.addSchema(drop_id(schema), filename).getSchema(reference); 75 | } 76 | 77 | // Save in the global scope so we can use this later from R 78 | global.validator = ret; 79 | } 80 | 81 | global.drop_id = function(x) { 82 | delete x.id; 83 | delete x.$id; 84 | return x; 85 | } 86 | 87 | global.imjv_create = function(meta_schema_version, schema) { 88 | // https://github.com/mafintosh/is-my-json-valid/issues/160 89 | if (meta_schema_version != "draft-04") { 90 | throw new Error("Only draft-04 json schema is supported"); 91 | } 92 | global.validator = imjv(schema); 93 | } 94 | 95 | global.ajv_call = function(value, errors, query) { 96 | var success = validator(jsonpath_eval(value, query)); 97 | var errors = (!success && errors ? validator.errors : null); 98 | return {"success": success, "errors": errors, "engine": "ajv"}; 99 | } 100 | 101 | global.imjv_call = function(value, errors, greedy) { 102 | var success = validator(value, {"greedy": greedy}, {"verbose": errors}); 103 | var errors = (!success && errors ? validator.errors : null); 104 | return {"success": success, "errors": errors, "engine": "imjv"}; 105 | } 106 | 107 | global.get_meta_schema_version = function(schema) { 108 | return schema.$schema; 109 | }; 110 | 111 | global.find_reference = function(x) { 112 | var deps = []; 113 | 114 | var f = function(x) { 115 | if (Array.isArray(x)) { 116 | // need to descend into arrays as they're used for things 117 | // like oneOf or anyOf constructs. 118 | x.forEach(f); 119 | } else if (typeof(x) === "object" && x !== null) { 120 | // From the JSON schema docs: 121 | // 122 | // > You will always use $ref as the only key in an 123 | // > object: any other keys you put there will be ignored 124 | // > by the validator 125 | // 126 | // though this turns not to be true empirically... 127 | if ("$ref" in x) { 128 | deps.push(x["$ref"]); 129 | } 130 | // Would be nicer with Object.values but that does not 131 | // work on travis apparently. 132 | Object.keys(x).forEach(function(k) {f(x[k]);}); 133 | } 134 | }; 135 | f(x); 136 | return deps; 137 | } 138 | 139 | // It might be nice to do this with jsonpath, but that does not seem 140 | // to work well with browserify. For now, we're going to accept 141 | // 'query' as a string corresponding to a single element 142 | global.jsonpath_eval = function(data, query) { 143 | if (query === null) { 144 | return(data); 145 | } 146 | if (data === null || Array.isArray(data) || typeof(data) !== "object") { 147 | throw new Error("Query only supported with object json"); 148 | } else if (!(query in data)) { 149 | throw new Error("Query did not match any element in the data"); 150 | } 151 | return data[query]; 152 | } 153 | 154 | 155 | global.typeIsAtomic = function(t) { 156 | // the const one might be overly generous; it might be that a 157 | // constant non-atomic type is allowed. 158 | return t === "string" || t === "number" || t === "boolean" || 159 | t == "enum" || t == "const" || t == "integer"; 160 | } 161 | 162 | global.unboxable = function(x) { 163 | return Array.isArray(x) && x.length === 1 && typeIsAtomic(typeof(x[0])); 164 | } 165 | 166 | global.fixUnboxable = function(x, schema) { 167 | var f = function(x, s, parent, index) { 168 | if (Array.isArray(x)) { 169 | if (typeIsAtomic(s.type) && unboxable(x)) { 170 | if (parent === null) { 171 | x = x[0]; 172 | } else { 173 | parent[index] = x[0]; 174 | } 175 | } else { 176 | var descend = x.length > 0 && 177 | typeof(x[0]) == "object" && 178 | s.hasOwnProperty("items") && 179 | (s.items.type === "object" || s.items.type == "array"); 180 | if (descend) { 181 | for (var i = 0; i < x.length; ++i) { 182 | f(x[i], s.items, x, i); 183 | } 184 | } 185 | } 186 | } else if (typeof(x) === "object" && x !== null) { 187 | if (s.type === "object") { 188 | var keys = Object.keys(s.properties); 189 | for (var i = 0; i < keys.length; ++i) { 190 | var k = keys[i]; 191 | if (x.hasOwnProperty(k)) { 192 | f(x[k], s.properties[k], x, k); 193 | } 194 | } 195 | } 196 | } 197 | return x; 198 | } 199 | 200 | x = f(x, schema.schema, null); 201 | return x; 202 | } 203 | 204 | global.safeSerialise = function(x) { 205 | var x = JSON.parse(x); 206 | return JSON.stringify(fixUnboxable(x, validator)); 207 | } 208 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonvalidate", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "webpack": "webpack" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "MIT", 12 | "description": "Javascript support for jsonvalidate", 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/ropensci/jsonvalidate.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/ropensci/jsonvalidate/issues" 19 | }, 20 | "dependencies": { 21 | "@babel/core": "^7.15.8", 22 | "@babel/preset-env": "^7.15.8", 23 | "ajv": "^8.5.0", 24 | "ajv-draft-04": "^1.0.0", 25 | "ajv-formats": "^2.1.0", 26 | "babel-loader": "^8.2.3", 27 | "core-js": "^3.19.0", 28 | "is-my-json-valid": "^2.17.2", 29 | "process": "^0.11.10", 30 | "terser-webpack-plugin": "^5.2.4", 31 | "util": "^0.12.4", 32 | "webpack": "^5.60.0" 33 | }, 34 | "homepage": "https://github.com/ropensci/jsonvalidate#readme", 35 | "devDependencies": { 36 | "webpack-cli": "^4.9.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /js/prepare: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | HERE=$(dirname $0) 4 | cd $HERE 5 | 6 | echo "Working in $(pwd)" 7 | 8 | rm -rf node_modules 9 | npm install 10 | npm run webpack 11 | -------------------------------------------------------------------------------- /js/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require("path"); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | 5 | module.exports = { 6 | mode: "none", 7 | target: ["web", "es5"], 8 | entry: "./in.js", 9 | output: { 10 | path: path.resolve(__dirname), 11 | filename: "bundle.js" 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.m?js$/, 17 | "exclude": [ 18 | /node_modules[\/]core-js/, 19 | /node_modules[\/]webpack[\/]buildin/ 20 | ], 21 | use: { 22 | loader: 'babel-loader', 23 | options: { 24 | presets: ['@babel/preset-env'] 25 | } 26 | } 27 | } 28 | ] 29 | }, 30 | optimization: { 31 | minimize: true, 32 | minimizer: [ 33 | new TerserPlugin({ 34 | extractComments: false, 35 | terserOptions: { 36 | format: { 37 | comments: false, 38 | } 39 | } 40 | }) 41 | ] 42 | }, 43 | plugins: [ 44 | new webpack.ProvidePlugin({ 45 | process: 'process/browser', 46 | }) 47 | ], 48 | resolve: { 49 | fallback: { 50 | util: require.resolve("util/") 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /man-roxygen/example-json_schema.R: -------------------------------------------------------------------------------- 1 | # A simple schema example: 2 | schema <- '{ 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "title": "Product", 5 | "description": "A product from Acme\'s catalog", 6 | "type": "object", 7 | "properties": { 8 | "id": { 9 | "description": "The unique identifier for a product", 10 | "type": "integer" 11 | }, 12 | "name": { 13 | "description": "Name of the product", 14 | "type": "string" 15 | }, 16 | "price": { 17 | "type": "number", 18 | "minimum": 0, 19 | "exclusiveMinimum": true 20 | }, 21 | "tags": { 22 | "type": "array", 23 | "items": { 24 | "type": "string" 25 | }, 26 | "minItems": 1, 27 | "uniqueItems": true 28 | } 29 | }, 30 | "required": ["id", "name", "price"] 31 | }' 32 | 33 | # Construct a schema object 34 | obj <- jsonvalidate::json_schema$new(schema) 35 | 36 | # Test if some (invalid) json conforms to the schema 37 | obj$validate("{}") 38 | 39 | # Get a (rather verbose) explanation about why this was invalid: 40 | obj$validate("{}", verbose = TRUE) 41 | 42 | # Test if some (valid) json conforms to the schema 43 | json <- '{ 44 | "id": 1, 45 | "name": "A green door", 46 | "price": 12.50, 47 | "tags": ["home", "green"] 48 | }' 49 | obj$validate(json) 50 | 51 | # The reverse; some R data that we want to serialise to conform with 52 | # this schema 53 | x <- list(id = 1, name = "apple", price = 0.50, tags = "fruit") 54 | 55 | # Note that id, name, price are unboxed here (not arrays) but tags is 56 | # a length-1 array 57 | obj$serialise(x) 58 | -------------------------------------------------------------------------------- /man-roxygen/example-json_serialise.R: -------------------------------------------------------------------------------- 1 | # This is the schema from ?json_validator 2 | schema <- '{ 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "title": "Product", 5 | "description": "A product from Acme\'s catalog", 6 | "type": "object", 7 | "properties": { 8 | "id": { 9 | "description": "The unique identifier for a product", 10 | "type": "integer" 11 | }, 12 | "name": { 13 | "description": "Name of the product", 14 | "type": "string" 15 | }, 16 | "price": { 17 | "type": "number", 18 | "minimum": 0, 19 | "exclusiveMinimum": true 20 | }, 21 | "tags": { 22 | "type": "array", 23 | "items": { 24 | "type": "string" 25 | }, 26 | "minItems": 1, 27 | "uniqueItems": true 28 | } 29 | }, 30 | "required": ["id", "name", "price"] 31 | }' 32 | 33 | # We're going to use a validator object below 34 | v <- jsonvalidate::json_validator(schema, "ajv") 35 | 36 | # And this is some data that we might generate in R that we want to 37 | # serialise using that schema 38 | x <- list(id = 1, name = "apple", price = 0.50, tags = "fruit") 39 | 40 | # If we serialise to json, then 'id', 'name' and "price' end up a 41 | # length 1-arrays 42 | jsonlite::toJSON(x) 43 | 44 | # ...and that fails validation 45 | v(jsonlite::toJSON(x)) 46 | 47 | # If we auto-unbox then 'fruit' ends up as a string and not an array, 48 | # also failing validation: 49 | jsonlite::toJSON(x, auto_unbox = TRUE) 50 | v(jsonlite::toJSON(x, auto_unbox = TRUE)) 51 | 52 | # Using json_serialise we can guide the serialisation process using 53 | # the schema: 54 | jsonvalidate::json_serialise(x, schema) 55 | 56 | # ...and this way we do pass validation: 57 | v(jsonvalidate::json_serialise(x, schema)) 58 | 59 | # It is typically much more efficient to construct a json_schema 60 | # object first and do both operations with it: 61 | obj <- jsonvalidate::json_schema$new(schema) 62 | json <- obj$serialise(x) 63 | obj$validate(json) 64 | -------------------------------------------------------------------------------- /man-roxygen/example-json_validate.R: -------------------------------------------------------------------------------- 1 | # A simple schema example: 2 | schema <- '{ 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "title": "Product", 5 | "description": "A product from Acme\'s catalog", 6 | "type": "object", 7 | "properties": { 8 | "id": { 9 | "description": "The unique identifier for a product", 10 | "type": "integer" 11 | }, 12 | "name": { 13 | "description": "Name of the product", 14 | "type": "string" 15 | }, 16 | "price": { 17 | "type": "number", 18 | "minimum": 0, 19 | "exclusiveMinimum": true 20 | }, 21 | "tags": { 22 | "type": "array", 23 | "items": { 24 | "type": "string" 25 | }, 26 | "minItems": 1, 27 | "uniqueItems": true 28 | } 29 | }, 30 | "required": ["id", "name", "price"] 31 | }' 32 | 33 | # Test if some (invalid) json conforms to the schema 34 | jsonvalidate::json_validate("{}", schema, verbose = TRUE) 35 | 36 | # Test if some (valid) json conforms to the schema 37 | json <- '{ 38 | "id": 1, 39 | "name": "A green door", 40 | "price": 12.50, 41 | "tags": ["home", "green"] 42 | }' 43 | jsonvalidate::json_validate(json, schema) 44 | 45 | # Test a fraction of a data against a reference into the schema: 46 | jsonvalidate::json_validate(json, schema, 47 | query = "tags", reference = "#/properties/tags", 48 | engine = "ajv", verbose = TRUE) 49 | -------------------------------------------------------------------------------- /man-roxygen/example-json_validator.R: -------------------------------------------------------------------------------- 1 | # A simple schema example: 2 | schema <- '{ 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "title": "Product", 5 | "description": "A product from Acme\'s catalog", 6 | "type": "object", 7 | "properties": { 8 | "id": { 9 | "description": "The unique identifier for a product", 10 | "type": "integer" 11 | }, 12 | "name": { 13 | "description": "Name of the product", 14 | "type": "string" 15 | }, 16 | "price": { 17 | "type": "number", 18 | "minimum": 0, 19 | "exclusiveMinimum": true 20 | }, 21 | "tags": { 22 | "type": "array", 23 | "items": { 24 | "type": "string" 25 | }, 26 | "minItems": 1, 27 | "uniqueItems": true 28 | } 29 | }, 30 | "required": ["id", "name", "price"] 31 | }' 32 | 33 | # Create a validator function 34 | v <- jsonvalidate::json_validator(schema) 35 | 36 | # Test if some (invalid) json conforms to the schema 37 | v("{}", verbose = TRUE) 38 | 39 | # Test if some (valid) json conforms to the schema 40 | v('{ 41 | "id": 1, 42 | "name": "A green door", 43 | "price": 12.50, 44 | "tags": ["home", "green"] 45 | }') 46 | 47 | # Using features from draft-06 or draft-07 requires the ajv engine: 48 | schema <- "{ 49 | '$schema': 'http://json-schema.org/draft-06/schema#', 50 | 'type': 'object', 51 | 'properties': { 52 | 'a': { 53 | 'const': 'foo' 54 | } 55 | } 56 | }" 57 | 58 | # Create the validator 59 | v <- jsonvalidate::json_validator(schema, engine = "ajv") 60 | 61 | # This confirms to the schema 62 | v('{"a": "foo"}') 63 | 64 | # But this does not 65 | v('{"a": "bar"}') 66 | -------------------------------------------------------------------------------- /man/json_schema.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/schema.R 3 | \name{json_schema} 4 | \alias{json_schema} 5 | \title{Interact with JSON schemas} 6 | \description{ 7 | Interact with JSON schemas, using them to validate 8 | json strings or serialise objects to JSON safely. 9 | 10 | This interface supersedes \link{json_schema} and changes 11 | some default arguments. While the old interface is not going 12 | away any time soon, users are encouraged to switch to this 13 | interface, which is what we will develop in the future. 14 | } 15 | \examples{ 16 | # This is the schema from ?json_validator 17 | schema <- '{ 18 | "$schema": "http://json-schema.org/draft-04/schema#", 19 | "title": "Product", 20 | "description": "A product from Acme\'s catalog", 21 | "type": "object", 22 | "properties": { 23 | "id": { 24 | "description": "The unique identifier for a product", 25 | "type": "integer" 26 | }, 27 | "name": { 28 | "description": "Name of the product", 29 | "type": "string" 30 | }, 31 | "price": { 32 | "type": "number", 33 | "minimum": 0, 34 | "exclusiveMinimum": true 35 | }, 36 | "tags": { 37 | "type": "array", 38 | "items": { 39 | "type": "string" 40 | }, 41 | "minItems": 1, 42 | "uniqueItems": true 43 | } 44 | }, 45 | "required": ["id", "name", "price"] 46 | }' 47 | 48 | # We're going to use a validator object below 49 | v <- jsonvalidate::json_validator(schema, "ajv") 50 | 51 | # And this is some data that we might generate in R that we want to 52 | # serialise using that schema 53 | x <- list(id = 1, name = "apple", price = 0.50, tags = "fruit") 54 | 55 | # If we serialise to json, then 'id', 'name' and "price' end up a 56 | # length 1-arrays 57 | jsonlite::toJSON(x) 58 | 59 | # ...and that fails validation 60 | v(jsonlite::toJSON(x)) 61 | 62 | # If we auto-unbox then 'fruit' ends up as a string and not an array, 63 | # also failing validation: 64 | jsonlite::toJSON(x, auto_unbox = TRUE) 65 | v(jsonlite::toJSON(x, auto_unbox = TRUE)) 66 | 67 | # Using json_serialise we can guide the serialisation process using 68 | # the schema: 69 | jsonvalidate::json_serialise(x, schema) 70 | 71 | # ...and this way we do pass validation: 72 | v(jsonvalidate::json_serialise(x, schema)) 73 | 74 | # It is typically much more efficient to construct a json_schema 75 | # object first and do both operations with it: 76 | obj <- jsonvalidate::json_schema$new(schema) 77 | json <- obj$serialise(x) 78 | obj$validate(json) 79 | } 80 | \section{Public fields}{ 81 | \if{html}{\out{
}} 82 | \describe{ 83 | \item{\code{schema}}{The parsed schema, cannot be rebound} 84 | 85 | \item{\code{engine}}{The name of the schema validation engine} 86 | } 87 | \if{html}{\out{
}} 88 | } 89 | \section{Methods}{ 90 | \subsection{Public methods}{ 91 | \itemize{ 92 | \item \href{#method-json_schema-new}{\code{json_schema$new()}} 93 | \item \href{#method-json_schema-validate}{\code{json_schema$validate()}} 94 | \item \href{#method-json_schema-serialise}{\code{json_schema$serialise()}} 95 | } 96 | } 97 | \if{html}{\out{
}} 98 | \if{html}{\out{}} 99 | \if{latex}{\out{\hypertarget{method-json_schema-new}{}}} 100 | \subsection{Method \code{new()}}{ 101 | Create a new \code{json_schema} object. 102 | \subsection{Usage}{ 103 | \if{html}{\out{
}}\preformatted{json_schema$new(schema, engine = "ajv", reference = NULL, strict = FALSE)}\if{html}{\out{
}} 104 | } 105 | 106 | \subsection{Arguments}{ 107 | \if{html}{\out{
}} 108 | \describe{ 109 | \item{\code{schema}}{Contents of the json schema, or a filename 110 | containing a schema.} 111 | 112 | \item{\code{engine}}{Specify the validation engine to use. Options are 113 | "ajv" (the default; "Another JSON Schema Validator") or "imjv" 114 | ("is-my-json-valid", the default everywhere in versions prior 115 | to 1.4.0, and the default for \link{json_validator}. 116 | \emph{Use of \code{ajv} is strongly recommended for all new code}.} 117 | 118 | \item{\code{reference}}{Reference within schema to use for validating 119 | against a sub-schema instead of the full schema passed in. 120 | For example if the schema has a 'definitions' list including a 121 | definition for a 'Hello' object, one could pass 122 | "#/definitions/Hello" and the validator would check that the json 123 | is a valid "Hello" object. Only available if \code{engine = "ajv"}.} 124 | 125 | \item{\code{strict}}{Set whether the schema should be parsed strictly or not. 126 | If in strict mode schemas will error to "prevent any unexpected 127 | behaviours or silently ignored mistakes in user schema". For example 128 | it will error if encounters unknown formats or unknown keywords. See 129 | https://ajv.js.org/strict-mode.html for details. Only available in 130 | \code{engine = "ajv"} and silently ignored for "imjv". 131 | Validate a json string against a schema.} 132 | } 133 | \if{html}{\out{
}} 134 | } 135 | } 136 | \if{html}{\out{
}} 137 | \if{html}{\out{}} 138 | \if{latex}{\out{\hypertarget{method-json_schema-validate}{}}} 139 | \subsection{Method \code{validate()}}{ 140 | \subsection{Usage}{ 141 | \if{html}{\out{
}}\preformatted{json_schema$validate( 142 | json, 143 | verbose = FALSE, 144 | greedy = FALSE, 145 | error = FALSE, 146 | query = NULL 147 | )}\if{html}{\out{
}} 148 | } 149 | 150 | \subsection{Arguments}{ 151 | \if{html}{\out{
}} 152 | \describe{ 153 | \item{\code{json}}{Contents of a json object, or a filename containing 154 | one.} 155 | 156 | \item{\code{verbose}}{Be verbose? If \code{TRUE}, then an attribute 157 | "errors" will list validation failures as a data.frame} 158 | 159 | \item{\code{greedy}}{Continue after the first error?} 160 | 161 | \item{\code{error}}{Throw an error on parse failure? If \code{TRUE}, 162 | then the function returns \code{NULL} on success (i.e., call 163 | only for the side-effect of an error on failure, like 164 | \code{stopifnot}).} 165 | 166 | \item{\code{query}}{A string indicating a component of the data to 167 | validate the schema against. Eventually this may support full 168 | \href{https://www.npmjs.com/package/jsonpath}{jsonpath} syntax, but 169 | for now this must be the name of an element within \code{json}. See 170 | the examples for more details. 171 | Serialise an R object to JSON with unboxing guided by the schema. 172 | See \link{json_serialise} for details on the problem and 173 | the algorithm.} 174 | } 175 | \if{html}{\out{
}} 176 | } 177 | } 178 | \if{html}{\out{
}} 179 | \if{html}{\out{}} 180 | \if{latex}{\out{\hypertarget{method-json_schema-serialise}{}}} 181 | \subsection{Method \code{serialise()}}{ 182 | \subsection{Usage}{ 183 | \if{html}{\out{
}}\preformatted{json_schema$serialise(object)}\if{html}{\out{
}} 184 | } 185 | 186 | \subsection{Arguments}{ 187 | \if{html}{\out{
}} 188 | \describe{ 189 | \item{\code{object}}{An R object to serialise} 190 | } 191 | \if{html}{\out{
}} 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /man/json_serialise.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/serialise.R 3 | \name{json_serialise} 4 | \alias{json_serialise} 5 | \title{Safe JSON serialisation} 6 | \usage{ 7 | json_serialise( 8 | object, 9 | schema, 10 | engine = "ajv", 11 | reference = NULL, 12 | strict = FALSE 13 | ) 14 | } 15 | \arguments{ 16 | \item{object}{An object to be serialised} 17 | 18 | \item{schema}{A schema (string or path to a string, suitable to be 19 | passed through to \link{json_validator} or a validator 20 | object itself.} 21 | 22 | \item{engine}{The engine to use. Only ajv is supported, and trying 23 | to use \code{imjv} will throw an error.} 24 | 25 | \item{reference}{Reference within schema to use for validating against a 26 | sub-schema instead of the full schema passed in. For example 27 | if the schema has a 'definitions' list including a definition for a 28 | 'Hello' object, one could pass "#/definitions/Hello" and the validator 29 | would check that the json is a valid "Hello" object. Only available if 30 | \code{engine = "ajv"}.} 31 | 32 | \item{strict}{Set whether the schema should be parsed strictly or not. 33 | If in strict mode schemas will error to "prevent any unexpected 34 | behaviours or silently ignored mistakes in user schema". For example 35 | it will error if encounters unknown formats or unknown keywords. See 36 | https://ajv.js.org/strict-mode.html for details. Only available in 37 | \code{engine = "ajv"}.} 38 | } 39 | \value{ 40 | A string, representing \code{object} in JSON format. As for 41 | \code{jsonlite::toJSON} we set the class attribute to be \code{json} to 42 | mark it as serialised json. 43 | } 44 | \description{ 45 | Safe serialisation of json with unboxing guided by the schema. 46 | } 47 | \details{ 48 | When using \link[jsonlite:fromJSON]{jsonlite::toJSON} we are forced to deal with the 49 | differences between R's types and those available in JSON. In 50 | particular: 51 | \itemize{ 52 | \item R has no scalar types so it is not clear if \code{1} should be 53 | serialised as a number or a vector of length 1; \code{jsonlite} 54 | provides support for "automatically unboxing" such values 55 | (assuming that length-1 vectors are scalars) or never unboxing 56 | them unless asked to using \link[jsonlite:unbox]{jsonlite::unbox} 57 | \item JSON has no date/time values and there are many possible string 58 | representations. 59 | \item JSON has no \link{data.frame} or \link{matrix} type and there are several 60 | ways of representing these in JSON, all equally valid (e.g., row-wise, 61 | column-wise or as an array of objects). 62 | \item The handling of \code{NULL} and missing values (\code{NA}, \code{NaN}) are different 63 | \item We need to chose the number of digits to write numbers out at, 64 | balancing precision and storage. 65 | } 66 | 67 | These issues are somewhat lessened when we have a schema because 68 | we know what our target type looks like. This function attempts 69 | to use the schema to guide serialisation of json safely. Currently 70 | it only supports detecting the appropriate treatment of length-1 71 | vectors, but we will expand functionality over time. 72 | 73 | For a user, this function provides an argument-free replacement 74 | for \code{jsonlite::toJSON}, accepting an R object and returning a 75 | string with the JSON representation of the object. Internally the 76 | algorithm is: 77 | \enumerate{ 78 | \item serialise the object with \link[jsonlite:fromJSON]{jsonlite::toJSON}, with 79 | \code{auto_unbox = FALSE} so that length-1 vectors are serialised as a 80 | length-1 arrays. 81 | \item operating entirely within JavaScript, deserialise the object 82 | with \code{JSON.parse}, traverse the object and its schema 83 | simultaneously looking for length-1 arrays where the schema 84 | says there should be scalar value and unboxing these, and 85 | re-serialise with \code{JSON.stringify} 86 | } 87 | 88 | There are several limitations to our current approach, and not all 89 | unboxable values will be found - at the moment we know that 90 | schemas contained within a \code{oneOf} block (or similar) will not be 91 | recursed into. 92 | } 93 | \section{Warning}{ 94 | Direct use of this function will be slow! If you are going to 95 | serialise more than one or two objects with a single schema, you 96 | should use the \code{serialise} method of a 97 | \link{json_schema} object which you create once and pass around. 98 | } 99 | 100 | \examples{ 101 | # This is the schema from ?json_validator 102 | schema <- '{ 103 | "$schema": "http://json-schema.org/draft-04/schema#", 104 | "title": "Product", 105 | "description": "A product from Acme\'s catalog", 106 | "type": "object", 107 | "properties": { 108 | "id": { 109 | "description": "The unique identifier for a product", 110 | "type": "integer" 111 | }, 112 | "name": { 113 | "description": "Name of the product", 114 | "type": "string" 115 | }, 116 | "price": { 117 | "type": "number", 118 | "minimum": 0, 119 | "exclusiveMinimum": true 120 | }, 121 | "tags": { 122 | "type": "array", 123 | "items": { 124 | "type": "string" 125 | }, 126 | "minItems": 1, 127 | "uniqueItems": true 128 | } 129 | }, 130 | "required": ["id", "name", "price"] 131 | }' 132 | 133 | # We're going to use a validator object below 134 | v <- jsonvalidate::json_validator(schema, "ajv") 135 | 136 | # And this is some data that we might generate in R that we want to 137 | # serialise using that schema 138 | x <- list(id = 1, name = "apple", price = 0.50, tags = "fruit") 139 | 140 | # If we serialise to json, then 'id', 'name' and "price' end up a 141 | # length 1-arrays 142 | jsonlite::toJSON(x) 143 | 144 | # ...and that fails validation 145 | v(jsonlite::toJSON(x)) 146 | 147 | # If we auto-unbox then 'fruit' ends up as a string and not an array, 148 | # also failing validation: 149 | jsonlite::toJSON(x, auto_unbox = TRUE) 150 | v(jsonlite::toJSON(x, auto_unbox = TRUE)) 151 | 152 | # Using json_serialise we can guide the serialisation process using 153 | # the schema: 154 | jsonvalidate::json_serialise(x, schema) 155 | 156 | # ...and this way we do pass validation: 157 | v(jsonvalidate::json_serialise(x, schema)) 158 | 159 | # It is typically much more efficient to construct a json_schema 160 | # object first and do both operations with it: 161 | obj <- jsonvalidate::json_schema$new(schema) 162 | json <- obj$serialise(x) 163 | obj$validate(json) 164 | } 165 | -------------------------------------------------------------------------------- /man/json_validate.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/validate.R 3 | \name{json_validate} 4 | \alias{json_validate} 5 | \title{Validate a json file} 6 | \usage{ 7 | json_validate( 8 | json, 9 | schema, 10 | verbose = FALSE, 11 | greedy = FALSE, 12 | error = FALSE, 13 | engine = "imjv", 14 | reference = NULL, 15 | query = NULL, 16 | strict = FALSE 17 | ) 18 | } 19 | \arguments{ 20 | \item{json}{Contents of a json object, or a filename containing 21 | one.} 22 | 23 | \item{schema}{Contents of the json schema, or a filename 24 | containing a schema.} 25 | 26 | \item{verbose}{Be verbose? If \code{TRUE}, then an attribute 27 | "errors" will list validation failures as a data.frame} 28 | 29 | \item{greedy}{Continue after the first error?} 30 | 31 | \item{error}{Throw an error on parse failure? If \code{TRUE}, 32 | then the function returns \code{NULL} on success (i.e., call 33 | only for the side-effect of an error on failure, like 34 | \code{stopifnot}).} 35 | 36 | \item{engine}{Specify the validation engine to use. Options are 37 | "imjv" (the default; which uses "is-my-json-valid") and "ajv" 38 | (Another JSON Schema Validator). The latter supports more 39 | recent json schema features.} 40 | 41 | \item{reference}{Reference within schema to use for validating against a 42 | sub-schema instead of the full schema passed in. For example 43 | if the schema has a 'definitions' list including a definition for a 44 | 'Hello' object, one could pass "#/definitions/Hello" and the validator 45 | would check that the json is a valid "Hello" object. Only available if 46 | \code{engine = "ajv"}.} 47 | 48 | \item{query}{A string indicating a component of the data to 49 | validate the schema against. Eventually this may support full 50 | \href{https://www.npmjs.com/package/jsonpath}{jsonpath} syntax, but 51 | for now this must be the name of an element within \code{json}. See 52 | the examples for more details.} 53 | 54 | \item{strict}{Set whether the schema should be parsed strictly or not. 55 | If in strict mode schemas will error to "prevent any unexpected 56 | behaviours or silently ignored mistakes in user schema". For example 57 | it will error if encounters unknown formats or unknown keywords. See 58 | https://ajv.js.org/strict-mode.html for details. Only available in 59 | \code{engine = "ajv"}.} 60 | } 61 | \description{ 62 | Validate a single json against a schema. This is a convenience 63 | wrapper around \code{json_validator(schema)(json)} or 64 | \code{json_schema$new(schema, engine = "ajv")$validate(json)}. See 65 | \code{\link[=json_validator]{json_validator()}} for further details. 66 | } 67 | \examples{ 68 | # A simple schema example: 69 | schema <- '{ 70 | "$schema": "http://json-schema.org/draft-04/schema#", 71 | "title": "Product", 72 | "description": "A product from Acme\'s catalog", 73 | "type": "object", 74 | "properties": { 75 | "id": { 76 | "description": "The unique identifier for a product", 77 | "type": "integer" 78 | }, 79 | "name": { 80 | "description": "Name of the product", 81 | "type": "string" 82 | }, 83 | "price": { 84 | "type": "number", 85 | "minimum": 0, 86 | "exclusiveMinimum": true 87 | }, 88 | "tags": { 89 | "type": "array", 90 | "items": { 91 | "type": "string" 92 | }, 93 | "minItems": 1, 94 | "uniqueItems": true 95 | } 96 | }, 97 | "required": ["id", "name", "price"] 98 | }' 99 | 100 | # Test if some (invalid) json conforms to the schema 101 | jsonvalidate::json_validate("{}", schema, verbose = TRUE) 102 | 103 | # Test if some (valid) json conforms to the schema 104 | json <- '{ 105 | "id": 1, 106 | "name": "A green door", 107 | "price": 12.50, 108 | "tags": ["home", "green"] 109 | }' 110 | jsonvalidate::json_validate(json, schema) 111 | 112 | # Test a fraction of a data against a reference into the schema: 113 | jsonvalidate::json_validate(json, schema, 114 | query = "tags", reference = "#/properties/tags", 115 | engine = "ajv", verbose = TRUE) 116 | } 117 | -------------------------------------------------------------------------------- /man/json_validator.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/validate.R 3 | \name{json_validator} 4 | \alias{json_validator} 5 | \title{Create a json validator} 6 | \usage{ 7 | json_validator(schema, engine = "imjv", reference = NULL, strict = FALSE) 8 | } 9 | \arguments{ 10 | \item{schema}{Contents of the json schema, or a filename 11 | containing a schema.} 12 | 13 | \item{engine}{Specify the validation engine to use. Options are 14 | "imjv" (the default; which uses "is-my-json-valid") and "ajv" 15 | (Another JSON Schema Validator). The latter supports more 16 | recent json schema features.} 17 | 18 | \item{reference}{Reference within schema to use for validating against a 19 | sub-schema instead of the full schema passed in. For example 20 | if the schema has a 'definitions' list including a definition for a 21 | 'Hello' object, one could pass "#/definitions/Hello" and the validator 22 | would check that the json is a valid "Hello" object. Only available if 23 | \code{engine = "ajv"}.} 24 | 25 | \item{strict}{Set whether the schema should be parsed strictly or not. 26 | If in strict mode schemas will error to "prevent any unexpected 27 | behaviours or silently ignored mistakes in user schema". For example 28 | it will error if encounters unknown formats or unknown keywords. See 29 | https://ajv.js.org/strict-mode.html for details. Only available in 30 | \code{engine = "ajv"}.} 31 | } 32 | \value{ 33 | A function that can be used to validate a 34 | schema. Additionally, the function has two attributes assigned: 35 | \code{v8} which is the JavaScript context (used internally) and 36 | \code{engine}, which contains the name of the engine used. 37 | } 38 | \description{ 39 | Create a validator that can validate multiple json files. 40 | } 41 | \section{Validation Engines}{ 42 | 43 | 44 | We support two different json validation engines, \code{imjv} 45 | ("is-my-json-valid") and \code{ajv} ("Another JSON 46 | Validator"). \code{imjv} was the original validator included in 47 | the package and remains the default for reasons of backward 48 | compatibility. However, users are encouraged to migrate to 49 | \code{ajv} as with it we support many more features, including 50 | nested schemas that span multiple files, meta schema versions 51 | later than draft-04, validating using a subschema, and 52 | validating a subset of an input data object. 53 | 54 | If your schema uses these features we will print a message to 55 | screen indicating that you should update when running 56 | interactively. We do not use a warning here as this will be 57 | disruptive to users. You can disable the message by setting the 58 | option \code{jsonvalidate.no_note_imjv} to \code{TRUE}. Consider using 59 | \code{\link[withr:with_options]{withr::with_options()}} (or simply \code{\link[=suppressMessages]{suppressMessages()}}) to 60 | scope this option if you want to quieten it within code you do 61 | not control. Alternatively, setting the option 62 | \code{jsonvalidate.no_note_imjv} to \code{FALSE} will print the message 63 | even non-interactively. 64 | 65 | Updating the engine should be simply a case of adding \code{engine = "ajv"} to your \code{json_validator} or \code{json_validate} 66 | calls, but you may see some issues when doing so. 67 | \itemize{ 68 | \item Your json now fails validation: We've seen this where schemas 69 | spanned several files and are silently ignored. By including 70 | these, your data may now fail validation and you will need to 71 | either fix the data or the schema. 72 | \item Your code depended on the exact payload returned by \code{imjv}: If 73 | you are inspecting the error result and checking numbers of 74 | errors, or even the columns used to describe the errors, you 75 | will likely need to update your code to accommodate the slightly 76 | different format of \code{ajv} 77 | \item Your schema is simply invalid: If you reference an invalid 78 | metaschema for example, jsonvalidate will fail 79 | } 80 | } 81 | 82 | \section{Using multiple files}{ 83 | 84 | 85 | Multiple files are supported. You can have a schema that references 86 | a file \code{child.json} using \code{{"$ref": "child.json"}}---in this case if 87 | \code{child.json} includes an \code{id} or \verb{$id} element it will be silently 88 | dropped and the filename used to reference the schema will be used 89 | as the schema id. 90 | 91 | The support is currently quite limited - it will not (yet) read 92 | sub-child schemas relative to child schema \verb{$id} url, and 93 | does not support reading from URLs (only local files are 94 | supported). 95 | } 96 | 97 | \examples{ 98 | # A simple schema example: 99 | schema <- '{ 100 | "$schema": "http://json-schema.org/draft-04/schema#", 101 | "title": "Product", 102 | "description": "A product from Acme\'s catalog", 103 | "type": "object", 104 | "properties": { 105 | "id": { 106 | "description": "The unique identifier for a product", 107 | "type": "integer" 108 | }, 109 | "name": { 110 | "description": "Name of the product", 111 | "type": "string" 112 | }, 113 | "price": { 114 | "type": "number", 115 | "minimum": 0, 116 | "exclusiveMinimum": true 117 | }, 118 | "tags": { 119 | "type": "array", 120 | "items": { 121 | "type": "string" 122 | }, 123 | "minItems": 1, 124 | "uniqueItems": true 125 | } 126 | }, 127 | "required": ["id", "name", "price"] 128 | }' 129 | 130 | # Create a validator function 131 | v <- jsonvalidate::json_validator(schema) 132 | 133 | # Test if some (invalid) json conforms to the schema 134 | v("{}", verbose = TRUE) 135 | 136 | # Test if some (valid) json conforms to the schema 137 | v('{ 138 | "id": 1, 139 | "name": "A green door", 140 | "price": 12.50, 141 | "tags": ["home", "green"] 142 | }') 143 | 144 | # Using features from draft-06 or draft-07 requires the ajv engine: 145 | schema <- "{ 146 | '$schema': 'http://json-schema.org/draft-06/schema#', 147 | 'type': 'object', 148 | 'properties': { 149 | 'a': { 150 | 'const': 'foo' 151 | } 152 | } 153 | }" 154 | 155 | # Create the validator 156 | v <- jsonvalidate::json_validator(schema, engine = "ajv") 157 | 158 | # This confirms to the schema 159 | v('{"a": "foo"}') 160 | 161 | # But this does not 162 | v('{"a": "bar"}') 163 | } 164 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | if (identical(Sys.getenv("NOT_CRAN"), "true")) { 2 | library(testthat) 3 | library(jsonvalidate) 4 | 5 | test_check("jsonvalidate") 6 | } 7 | -------------------------------------------------------------------------------- /tests/testthat/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "type": "object", 4 | "properties": { 5 | "hello": { 6 | "required": true, 7 | "type": "string" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/testthat/schema2.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": ["hello"], 3 | "type": "object", 4 | "properties": { 5 | "hello": { 6 | "type": "string" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/testthat/test-read.R: -------------------------------------------------------------------------------- 1 | test_that("can't read empty input", { 2 | ct <- jsonvalidate_js() 3 | expect_error(read_schema(NULL, ct), 4 | "zero length input") 5 | expect_error(read_schema(character(0), ct), 6 | "zero length input") 7 | }) 8 | 9 | 10 | test_that("must read character input", { 11 | ct <- jsonvalidate_js() 12 | expect_error(read_schema(1, ct), 13 | "Expected a character vector") 14 | }) 15 | 16 | 17 | test_that("sensible error on missing files", { 18 | ct <- jsonvalidate_js() 19 | a <- c( 20 | '{', 21 | '"$ref": "b.json"', 22 | '}') 23 | b <- c( 24 | '{', 25 | '"$ref": "c.json"', 26 | '}') 27 | c <- c( 28 | '{', 29 | ' "type": "string"', 30 | '}') 31 | path <- tempfile() 32 | dir.create(path) 33 | writeLines(a, file.path(path, "a.json")) 34 | writeLines(b, file.path(path, "b.json")) 35 | expect_error( 36 | read_schema(file.path(path, "b.json"), ct), 37 | "While reading 'b.json' > 'c.json'\nDid not find schema file 'c.json'", 38 | class = "jsonvalidate_read_error") 39 | expect_error( 40 | read_schema(file.path(path, "a.json"), ct), 41 | paste0("While reading 'a.json' > 'b.json' > 'c.json'\n", 42 | "Did not find schema file 'c.json'"), 43 | class = "jsonvalidate_read_error") 44 | }) 45 | 46 | 47 | test_that("Read recursive schema", { 48 | ct <- jsonvalidate_js() 49 | sexpression <- c( 50 | '{', 51 | ' "oneOf": [', 52 | ' {"type": "string"},', 53 | ' {"type": "number"},', 54 | ' {"type": "array", "items": {"$ref": "sexpression.json"}}', 55 | ']}') 56 | 57 | path <- tempfile() 58 | dir.create(path) 59 | p <- file.path(path, "sexpression.json") 60 | writeLines(sexpression, p) 61 | dat <- read_schema(p, ct) 62 | expect_equal(length(dat$dependencies), 1) 63 | expect_equal(jsonlite::fromJSON(dat$dependencies)$id, "sexpression.json") 64 | 65 | v <- json_validator(p, engine = "ajv") 66 | expect_false(v("{}")) 67 | expect_true(v('["a"]')) 68 | expect_true(v('["a", ["b", "c", 3]]')) 69 | }) 70 | 71 | 72 | test_that("can't read external schemas", { 73 | ct <- jsonvalidate_js() 74 | a <- c( 75 | '{', 76 | '"$ref": "https://example.com/schema.json"', 77 | '}') 78 | expect_error(read_schema(a, ct), 79 | "Don't yet support protocol-based sub schemas") 80 | }) 81 | 82 | 83 | test_that("Conflicting schema versions", { 84 | ct <- jsonvalidate_js() 85 | a <- c( 86 | '{', 87 | ' "$schema": "http://json-schema.org/draft-07/schema#",', 88 | ' "$ref": "b.json"', 89 | '}') 90 | b <- c( 91 | '{', 92 | ' "$schema": "http://json-schema.org/draft-04/schema#",', 93 | ' "type": "string"', 94 | '}') 95 | path <- tempfile() 96 | dir.create(path) 97 | writeLines(a, file.path(path, "a.json")) 98 | writeLines(b, file.path(path, "b.json")) 99 | expect_error( 100 | read_schema(file.path(path, "a.json"), ct), 101 | "Conflicting subschema versions used:\n - draft-04: b.json") 102 | expect_error( 103 | with_dir(path, read_schema(a, ct)), 104 | "Conflicting subschema versions used:\n.+- draft-07: \\(input string\\)") 105 | writeLines(sub("-04", "-07", b), file.path(path, "b.json")) 106 | x <- read_schema(file.path(path, "a.json"), ct) 107 | expect_equal(x$meta_schema_version, "draft-07") 108 | }) 109 | 110 | 111 | test_that("Sensible reporting on syntax error", { 112 | ct <- jsonvalidate_js() 113 | parent <- c( 114 | '{', 115 | ' "type": "object",', 116 | ' "properties": {', 117 | ' "hello": {', 118 | ' "$ref": "child.json"', 119 | ' }', 120 | ' },', 121 | ' "required": ["hello"],', 122 | ' "additionalProperties": false', 123 | '}') 124 | child <- c( 125 | '{', 126 | ' "id": "child"', 127 | ' "type": "string"', 128 | '}') 129 | path <- tempfile() 130 | dir.create(path) 131 | writeLines(parent, file.path(path, "parent.json")) 132 | writeLines(child, file.path(path, "child.json")) 133 | expect_error( 134 | read_schema(file.path(path, "parent.json"), ct), 135 | "While reading 'parent.json' > 'child.json'", 136 | class = "jsonvalidate_read_error") 137 | }) 138 | 139 | 140 | test_that("schema string vs filename detection", { 141 | expect_false(read_schema_is_filename("''")) 142 | expect_false(read_schema_is_filename('""')) 143 | expect_false(read_schema_is_filename('{}')) 144 | expect_true(read_schema_is_filename('/foo/bar.json')) 145 | expect_true(read_schema_is_filename('bar.json')) 146 | expect_true(read_schema_is_filename('bar')) 147 | 148 | expect_false(read_schema_is_filename(character())) 149 | expect_false(read_schema_is_filename(c("a", "b"))) 150 | expect_false(read_schema_is_filename(I('/foo/bar.json'))) 151 | }) 152 | 153 | 154 | test_that("sensible error if reading missing schema", { 155 | expect_error( 156 | read_schema("/file/that/does/not/exist.json"), 157 | "Schema '/file/that/does/not/exist.json' looks like a filename but") 158 | }) 159 | 160 | test_that("can reference subsets of other schema", { 161 | ct <- jsonvalidate_js() 162 | a <- c( 163 | '{', 164 | '"$ref": "b.json#/definitions/b"', 165 | '}') 166 | b <- c( 167 | '{', 168 | ' "definitions": {', 169 | ' "b": {', 170 | ' "type": "string"', 171 | ' }', 172 | ' }', 173 | '}') 174 | path <- tempfile() 175 | dir.create(path) 176 | writeLines(a, file.path(path, "a.json")) 177 | writeLines(b, file.path(path, "b.json")) 178 | schema <- read_schema(file.path(path, "a.json"), ct) 179 | expect_equal(length(schema$dependencies), 1) 180 | expect_equal(jsonlite::fromJSON(schema$dependencies)$id, "b.json") 181 | }) 182 | -------------------------------------------------------------------------------- /tests/testthat/test-serialise.R: -------------------------------------------------------------------------------- 1 | test_that("Can safely serialise a json object using a schema", { 2 | schema <- '{ 3 | "type": "object", 4 | "properties": { 5 | "a": { 6 | "type": "string" 7 | }, 8 | "b": { 9 | "type": "array", 10 | "items": { 11 | "type": "string" 12 | } 13 | } 14 | } 15 | }' 16 | 17 | v <- json_schema$new(schema, "ajv") 18 | x <- list(a = "x", b = "y") 19 | str <- v$serialise(x) 20 | expect_equal(str, structure('{"a":"x","b":["y"]}', class = "json")) 21 | expect_true(v$validate(str)) 22 | expect_equal(json_serialise(x, schema), str) 23 | }) 24 | 25 | 26 | test_that("Can't use imjv with serialise", { 27 | v <- json_schema$new("{}", "imjv") 28 | x <- list(a = "x", b = "y") 29 | expect_error( 30 | v$serialise(x), 31 | "json_serialise is only supported with engine 'ajv'") 32 | }) 33 | -------------------------------------------------------------------------------- /tests/testthat/test-util.R: -------------------------------------------------------------------------------- 1 | test_that("get_string error cases", { 2 | expect_error(get_string(character(0), "thing"), 3 | "zero length input for thing") 4 | expect_error(get_string(1, "thing"), 5 | "Expected a character vector for thing") 6 | }) 7 | 8 | 9 | test_that("get_string reads files as a string", { 10 | path <- tempfile() 11 | writeLines(c("some", "test"), path) 12 | expect_equal(get_string(path), "some\ntest") 13 | }) 14 | 15 | 16 | test_that("detect probable files", { 17 | path <- tempfile() 18 | expect_false(refers_to_file('{"a": 1}')) 19 | expect_false(refers_to_file(structure("1", class = "json"))) 20 | expect_false(refers_to_file(c("a", "b"))) 21 | expect_false(refers_to_file(path)) 22 | writeLines(c("some", "test"), path) 23 | expect_true(refers_to_file(path)) 24 | }) 25 | 26 | 27 | test_that("get_string concatenates character vectors", { 28 | expect_equal(get_string(c("some", "text")), 29 | "some\ntext") 30 | }) 31 | 32 | 33 | test_that("get_string passes along strings", { 34 | expect_equal(get_string("some\ntext"), 35 | "some\ntext") 36 | ## Probably not ideal: 37 | expect_equal(get_string("file_that_does_not_exist.json"), 38 | "file_that_does_not_exist.json") 39 | }) 40 | 41 | 42 | test_that("control printing imjv notice", { 43 | testthat::skip_if_not_installed("withr") 44 | withr::with_options( 45 | list(jsonvalidate.no_note_imjv = NULL), 46 | expect_message(note_imjv("note", TRUE), "note")) 47 | withr::with_options( 48 | list(jsonvalidate.no_note_imjv = FALSE), 49 | expect_message(note_imjv("note", TRUE), "note")) 50 | withr::with_options( 51 | list(jsonvalidate.no_note_imjv = TRUE), 52 | expect_silent(note_imjv("note", TRUE))) 53 | withr::with_options( 54 | list(jsonvalidate.no_note_imjv = NULL), 55 | expect_silent(note_imjv("note", FALSE))) 56 | withr::with_options( 57 | list(jsonvalidate.no_note_imjv = FALSE), 58 | expect_message(note_imjv("note", FALSE), "note")) 59 | withr::with_options( 60 | list(jsonvalidate.no_note_imjv = TRUE), 61 | expect_silent(note_imjv("note", TRUE))) 62 | }) 63 | 64 | test_that("can check if path includes dir", { 65 | expect_false(path_includes_dir(NULL)) 66 | expect_false(path_includes_dir("file.json")) 67 | expect_true(path_includes_dir("the/file.json")) 68 | }) 69 | 70 | test_that("can read file with no trailing newline", { 71 | path <- tempfile() 72 | writeLines("12345678", path, sep="") 73 | 74 | # Check that we wrote just what we wanted and no more. 75 | expect_equal(file.info(path)$size, 8) 76 | 77 | result <- expect_silent(get_string(path)) 78 | expect_equal(result, "12345678") 79 | }) 80 | -------------------------------------------------------------------------------- /tests/testthat/test-validator.R: -------------------------------------------------------------------------------- 1 | ## NOTE: so far as I can see this is not valid json, nor is it 2 | ## sensible json schema (the 'required' kw should be an array) 3 | test_that("is-my-json-valid", { 4 | str <- "{ 5 | required: true, 6 | type: 'object', 7 | properties: { 8 | hello: { 9 | required: true, 10 | type: 'string' 11 | } 12 | } 13 | }" 14 | 15 | v <- json_validator(str, engine = "imjv") 16 | expect_false(v("{}")) 17 | expect_true(v("{hello: 'world'}")) 18 | 19 | expect_false(json_validate("{}", str, engine = "imjv")) 20 | expect_true(json_validate("{hello: 'world'}", str, engine = "imjv")) 21 | 22 | f <- tempfile() 23 | writeLines(str, f) 24 | v <- json_validator(f, engine = "imjv") 25 | expect_false(v("{}")) 26 | expect_true(v("{hello: 'world'}")) 27 | 28 | v <- json_validator("schema.json", engine = "imjv") 29 | expect_error(v("{}", error = TRUE), 30 | "data.hello: is required", 31 | class = "validation_error") 32 | expect_true(v("{hello: 'world'}", error = TRUE)) 33 | }) 34 | 35 | 36 | test_that("simple case works", { 37 | schema <- str <- '{ 38 | "type": "object", 39 | required: ["hello"], 40 | "properties": { 41 | "hello": { 42 | "type": "string" 43 | } 44 | } 45 | }' 46 | v <- json_validator(str, "ajv") 47 | expect_false(v("{}")) 48 | expect_true(v("{hello: 'world'}")) 49 | 50 | expect_false(json_validate("{}", str)) 51 | expect_true(json_validate("{hello: 'world'}", str)) 52 | 53 | f <- tempfile() 54 | writeLines(str, f) 55 | v <- json_validator(f) 56 | expect_false(v("{}")) 57 | expect_true(v("{hello: 'world'}")) 58 | 59 | v <- json_validator("schema2.json", "ajv") 60 | expect_error(v("{}", error = TRUE), "hello", class = "validation_error") 61 | expect_true(v("{hello: 'world'}", error = TRUE)) 62 | }) 63 | 64 | 65 | test_that("verbose output", { 66 | schema <- str <- '{ 67 | "type": "object", 68 | required: ["hello"], 69 | "properties": { 70 | "hello": { 71 | "type": "string" 72 | } 73 | } 74 | }' 75 | v <- json_validator(str, "ajv") 76 | res <- v("{}", verbose = TRUE) 77 | expect_false(res) 78 | expect_s3_class(attr(res, "errors"), "data.frame") 79 | }) 80 | 81 | 82 | test_that("const keyword is supported in draft-06, not draft-04", { 83 | schema <- "{ 84 | '$schema': 'http://json-schema.org/draft-04/schema#', 85 | 'type': 'object', 86 | 'properties': { 87 | 'a': { 88 | 'const': 'foo' 89 | } 90 | } 91 | }" 92 | 93 | expect_true(json_validate("{'a': 'foo'}", schema, engine = "ajv")) 94 | expect_true(json_validate("{'a': 'bar'}", schema, engine = "ajv")) 95 | 96 | ## Switch to draft-06 97 | schema <- gsub("draft-04", "draft-06", schema) 98 | 99 | expect_true(json_validate("{'a': 'foo'}", schema, engine = "ajv")) 100 | expect_false(json_validate("{'a': 'bar'}", schema, engine = "ajv")) 101 | }) 102 | 103 | test_that("if/then/else keywords are supported in draft-07, not draft-04", { 104 | schema <- "{ 105 | '$schema': 'http://json-schema.org/draft-04/schema#', 106 | 'type': 'object', 107 | 'if': { 108 | 'properties': { 109 | 'a': {'type': 'number', 'minimum': 1} 110 | } 111 | }, 112 | 'then': { 113 | 'required': ['b'] 114 | }, 115 | 'else': { 116 | 'required': ['c'] 117 | } 118 | }" 119 | 120 | expect_true(json_validate("{'a': 5, 'b': 5}", schema, engine = "ajv")) 121 | expect_true(json_validate("{'a': 0, 'b': 5}", schema, engine = "ajv")) 122 | 123 | ## Switch to draft-07 124 | schema <- gsub("draft-04", "draft-07", schema) 125 | 126 | expect_true(json_validate("{'a': 5, 'b': 5}", schema, engine = "ajv")) 127 | expect_false(json_validate("{'a': 5, 'c': 5}", schema, engine = "ajv")) 128 | }) 129 | 130 | 131 | test_that("subschema validation works", { 132 | schema <- '{ 133 | "$schema": "http://json-schema.org/draft-06/schema#", 134 | "definitions": { 135 | "Goodbye": { 136 | type: "object", 137 | properties: {"goodbye": {type: "string"}}, 138 | "required": ["goodbye"] 139 | }, 140 | "Hello": { 141 | type: "object", 142 | properties: {"hello": {type: "string"}}, 143 | "required": ["hello"] 144 | }, 145 | "Conversation": { 146 | "anyOf": [ 147 | {"$ref": "#/definitions/Hello"}, 148 | {"$ref": "#/definitions/Goodbye"}, 149 | ] 150 | } 151 | }, 152 | "$ref": "#/definitions/Conversation" 153 | }' 154 | 155 | val_goodbye <- json_validator(schema, "ajv", "#/definitions/Goodbye") 156 | 157 | expect_true(val_goodbye("{'goodbye': 'failure'}")) 158 | expect_false(val_goodbye("{'hello': 'failure'}")) 159 | 160 | val_hello <- json_validator(schema, "ajv", "#/definitions/Hello") 161 | 162 | expect_false(val_hello("{'goodbye': 'failure'}")) 163 | expect_true(val_hello("{'hello': 'failure'}")) 164 | }) 165 | 166 | 167 | test_that("can't use subschema reference with imjv", { 168 | expect_error(json_validator("{}", engine = "imjv", 169 | reference = "definitions/sub"), 170 | "subschema validation only supported with engine 'ajv'") 171 | }) 172 | 173 | test_that("can't use nested schemas with imjv", { 174 | testthat::skip_if_not_installed("withr") 175 | parent <- c( 176 | '{', 177 | ' "type": "object",', 178 | ' "properties": {', 179 | ' "hello": {', 180 | ' "$ref": "child.json"', 181 | ' }', 182 | ' },', 183 | ' "required": ["hello"],', 184 | ' "additionalProperties": false', 185 | '}') 186 | child <- c( 187 | '{', 188 | ' "type": "string"', 189 | '}') 190 | path <- tempfile() 191 | dir.create(path) 192 | writeLines(parent, file.path(path, "parent.json")) 193 | writeLines(child, file.path(path, "child.json")) 194 | 195 | withr::with_options( 196 | list(jsonvalidate.no_note_imjv = FALSE), 197 | expect_message( 198 | v <- json_validator(file.path(path, "parent.json"), engine = "imjv"), 199 | "Schema references are only supported with engine 'ajv'")) 200 | ## We incorrectly don't find this invalid, because we never read the 201 | ## child schema; the user should have used ajv! 202 | expect_true(v('{"hello": 1}')) 203 | }) 204 | 205 | 206 | test_that("can't use invalid engines", { 207 | expect_error(json_validator("{}", engine = "magic"), 208 | "Unknown engine 'magic'") 209 | }) 210 | 211 | 212 | test_that("can't use new schema versions with imjv", { 213 | testthat::skip_if_not_installed("withr") 214 | schema <- "{ 215 | '$schema': 'http://json-schema.org/draft-07/schema#', 216 | 'type': 'object', 217 | 'properties': { 218 | 'a': { 219 | 'const': 'foo' 220 | } 221 | } 222 | }" 223 | withr::with_options( 224 | list(jsonvalidate.no_note_imjv = FALSE), 225 | expect_message( 226 | v <- json_schema$new(schema, "imjv"), 227 | "meta schema version other than 'draft-04' is only supported with")) 228 | ## We incorrectly don't find this invalid, because imjv does not 229 | ## understand the const keyword. 230 | expect_true(v$validate('{"a": "bar"}')) 231 | }) 232 | 233 | 234 | test_that("Simple file references work", { 235 | parent <- c( 236 | '{', 237 | ' "type": "object",', 238 | ' "properties": {', 239 | ' "hello": {', 240 | ' "$ref": "child.json"', 241 | ' }', 242 | ' },', 243 | ' "required": ["hello"],', 244 | ' "additionalProperties": false', 245 | '}') 246 | child <- c( 247 | '{', 248 | ' "type": "string"', 249 | '}') 250 | path <- tempfile() 251 | dir.create(path) 252 | writeLines(parent, file.path(path, "parent.json")) 253 | writeLines(child, file.path(path, "child.json")) 254 | 255 | v <- json_validator(file.path(path, "parent.json"), engine = "ajv") 256 | expect_false(v("{}")) 257 | expect_true(v('{"hello": "world"}')) 258 | }) 259 | 260 | 261 | test_that("Referenced schemas have their ids replaced", { 262 | parent <- c( 263 | '{', 264 | ' "type": "object",', 265 | ' "properties": {', 266 | ' "hello": {', 267 | ' "$ref": "child.json"', 268 | ' }', 269 | ' },', 270 | ' "required": ["hello"],', 271 | ' "additionalProperties": false', 272 | '}') 273 | child <- c( 274 | '{', 275 | ' "id": "child",', 276 | ' "type": "string"', 277 | '}') 278 | path <- tempfile() 279 | dir.create(path) 280 | writeLines(parent, file.path(path, "parent.json")) 281 | writeLines(child, file.path(path, "child.json")) 282 | 283 | expect_silent( 284 | v <- json_validator(file.path(path, "parent.json"), engine = "ajv")) 285 | expect_false(v("{}")) 286 | expect_true(v('{"hello": "world"}')) 287 | }) 288 | 289 | 290 | test_that("file references in subdirectories work", { 291 | parent <- c( 292 | '{', 293 | ' "type": "object",', 294 | ' "properties": {', 295 | ' "hello": {', 296 | ' "$ref": "sub/child.json"', 297 | ' }', 298 | ' },', 299 | ' "required": ["hello"],', 300 | ' "additionalProperties": false', 301 | '}') 302 | child <- c( 303 | '{', 304 | ' "type": "string"', 305 | '}') 306 | path <- tempfile() 307 | dir.create(path) 308 | subdir <- file.path(path, "sub") 309 | dir.create(subdir) 310 | writeLines(parent, file.path(path, "parent.json")) 311 | writeLines(child, file.path(subdir, "child.json")) 312 | 313 | v <- json_validator(file.path(path, "parent.json"), engine = "ajv") 314 | expect_false(v("{}")) 315 | expect_true(v('{"hello": "world"}')) 316 | }) 317 | 318 | 319 | test_that("chained file references work", { 320 | parent <- c( 321 | '{', 322 | ' "type": "object",', 323 | ' "properties": {', 324 | ' "hello": {', 325 | ' "$ref": "sub/middle.json"', 326 | ' }', 327 | ' },', 328 | ' "required": ["hello"],', 329 | ' "additionalProperties": false', 330 | '}') 331 | middle <- c( 332 | '{', 333 | ' "type": "object",', 334 | ' "properties": {', 335 | ' "greeting": {', 336 | ' "$ref": "child.json"', 337 | ' }', 338 | ' },', 339 | ' "required": ["greeting"],', 340 | ' "additionalProperties": false', 341 | '}') 342 | child <- c( 343 | '{', 344 | ' "type": "string"', 345 | '}') 346 | path <- tempfile() 347 | dir.create(path) 348 | subdir <- file.path(path, "sub") 349 | dir.create(subdir) 350 | writeLines(parent, file.path(path, "parent.json")) 351 | writeLines(middle, file.path(subdir, "middle.json")) 352 | writeLines(child, file.path(subdir, "child.json")) 353 | 354 | v <- json_validator(file.path(path, "parent.json"), engine = "ajv") 355 | expect_false(v("{}")) 356 | expect_true(v('{"hello": { "greeting": "world"}}')) 357 | expect_false(v('{"hello": { "greeting": 2}}')) 358 | }) 359 | 360 | 361 | test_that("absolute file references throw error", { 362 | parent <- c( 363 | '{', 364 | ' "type": "object",', 365 | ' "properties": {', 366 | ' "greeting": {', 367 | ' "$ref": "%s"', 368 | ' },', 369 | ' "address": {', 370 | ' "$ref": "%s"', 371 | ' }', 372 | ' },', 373 | ' "required": ["greeting", "address"],', 374 | ' "additionalProperties": false', 375 | '}') 376 | child <- c( 377 | '{', 378 | ' "type": "string"', 379 | '}') 380 | path <- tempfile() 381 | dir.create(path) 382 | child_path1 <- file.path(path, "child1.json") 383 | writeLines(child, child_path1) 384 | child_path2 <- file.path(path, "child2.json") 385 | writeLines(child, child_path2) 386 | parent_path <- file.path(path, "parent.json") 387 | writeLines(sprintf(paste0(parent, collapse = "\n"), 388 | normalizePath(child_path1), normalizePath(child_path2)), 389 | parent_path) 390 | 391 | expect_error(json_validator(parent_path, engine = "ajv"), 392 | "'\\$ref' paths must be relative, got absolute path\\(s\\)") 393 | }) 394 | 395 | 396 | test_that("chained file references return useful error", { 397 | parent <- c( 398 | '{', 399 | ' "type": "object",', 400 | ' "properties": {', 401 | ' "hello": {', 402 | ' "$ref": "sub/middle.json"', 403 | ' }', 404 | ' },', 405 | ' "required": ["hello"],', 406 | ' "additionalProperties": false', 407 | '}') 408 | middle <- c( 409 | '{', 410 | ' "type": "object",', 411 | ' "properties": {', 412 | ' "greeting": {', 413 | ' "$ref": "sub/child.json"', 414 | ' }', 415 | ' },', 416 | ' "required": ["greeting"],', 417 | ' "additionalProperties": false', 418 | '}') 419 | child <- c( 420 | '{', 421 | ' "type": "string"', 422 | '}') 423 | path <- tempfile() 424 | dir.create(path) 425 | subdir <- file.path(path, "sub") 426 | dir.create(subdir) 427 | writeLines(parent, file.path(path, "parent.json")) 428 | writeLines(middle, file.path(subdir, "middle.json")) 429 | writeLines(child, file.path(subdir, "child.json")) 430 | 431 | expect_error( 432 | json_validator(file.path(path, "parent.json"), engine = "ajv"), 433 | "Did not find schema file 'sub/child.json' relative to 'sub/middle.json'") 434 | }) 435 | 436 | 437 | test_that("Can validate fraction of a json object", { 438 | schema <- c( 439 | '{', 440 | ' "type": "object",', 441 | ' "properties": {', 442 | ' "x": {', 443 | ' type: "string"', 444 | ' },', 445 | ' },', 446 | ' "required": ["x"]', 447 | '}') 448 | data <- c( 449 | '{', 450 | ' "a": {', 451 | ' "x": "string"', 452 | ' },', 453 | ' "b": {', 454 | ' y: 1', 455 | ' }', 456 | '}') 457 | 458 | expect_false(json_validate(data, schema, engine = "ajv")) 459 | expect_true(json_validate(data, schema, engine = "ajv", query = "a")) 460 | expect_false(json_validate(data, schema, engine = "ajv", query = "b")) 461 | 462 | expect_error( 463 | json_validate(data, schema, engine = "imjv", query = "c"), 464 | "Queries are only supported with engine 'ajv'") 465 | 466 | expect_error( 467 | json_validate(data, schema, engine = "ajv", query = "c"), 468 | "Query did not match any element in the data") 469 | 470 | expect_error( 471 | json_validate("[]", schema, engine = "ajv", query = "c"), 472 | "Query only supported with object json") 473 | expect_error( 474 | json_validate("null", schema, engine = "ajv", query = "c"), 475 | "Query only supported with object json") 476 | }) 477 | 478 | 479 | test_that("complex jsonpath queries are rejected", { 480 | msg <- "Full json-path support is not implemented" 481 | expect_error(query_validate("foo/bar"), msg) 482 | expect_error(query_validate("foo?"), msg) 483 | expect_error(query_validate("$foo"), msg) 484 | expect_error(query_validate("foo(bar)"), msg) 485 | expect_error(query_validate("foo[0]"), msg) 486 | expect_error(query_validate("foo@bar"), msg) 487 | }) 488 | 489 | 490 | test_that("simple jsonpath is passed along", { 491 | expect_identical(query_validate("foo"), "foo") 492 | expect_identical(query_validate(NULL), V8::JS("null")) 493 | }) 494 | 495 | 496 | test_that("stray null values in a schema are ok", { 497 | ## This is stripped down version of the vegalite schema 3.3.0 which 498 | ## includes a block 499 | ## 500 | ## "invalidValues": { 501 | ## "description": "Defines how Vega-Lite should handle ...", 502 | ## "enum": [ 503 | ## "filter", 504 | ## null 505 | ## ], 506 | ## "type": [ 507 | ## "string", 508 | ## "null" 509 | ## ] 510 | ## }, 511 | ## 512 | ## which used to break the reference detection by throwing an error 513 | ## when the schema was read. 514 | schema <- '{ 515 | "type": "object", 516 | "properties": { 517 | "a": { 518 | "enum": ["value", null] 519 | } 520 | } 521 | }' 522 | ct <- jsonvalidate_js() 523 | expect_error(read_schema(schema, ct), NA) 524 | }) 525 | 526 | 527 | test_that("Referencing definition in another file works", { 528 | parent <- c( 529 | '{', 530 | ' "type": "object",', 531 | ' "properties": {', 532 | ' "hello": {', 533 | ' "$ref": "child.json#/definitions/greeting"', 534 | ' }', 535 | ' },', 536 | ' "required": ["hello"],', 537 | ' "additionalProperties": false', 538 | '}') 539 | child <- c( 540 | '{', 541 | ' "definitions": {', 542 | ' "greeting": {', 543 | ' "type": "string"', 544 | ' }', 545 | ' }', 546 | '}') 547 | path <- tempfile() 548 | dir.create(path) 549 | writeLines(parent, file.path(path, "parent.json")) 550 | writeLines(child, file.path(path, "child.json")) 551 | 552 | v <- json_validator(file.path(path, "parent.json"), engine = "ajv") 553 | expect_false(v("{}")) 554 | expect_true(v('{"hello": "world"}')) 555 | invalid <- v('{"hello": ["thing"]}', verbose = TRUE) 556 | expect_false(invalid) 557 | error <- attr(invalid, "errors", TRUE) 558 | expect_equal(error$schemaPath, "child.json#/definitions/greeting/type") 559 | expect_equal(error$message, "must be string") 560 | }) 561 | 562 | 563 | test_that("schema can contain IDs", { 564 | schema <- c( 565 | '{', 566 | ' "$id": "http://example.com/schemas/thing.json",', 567 | ' "type": "object",', 568 | ' "properties": {', 569 | ' "hello": {', 570 | ' "type": "string"', 571 | ' }', 572 | ' },', 573 | ' "required": ["hello"],', 574 | ' "additionalProperties": false', 575 | '}') 576 | 577 | path <- tempfile() 578 | dir.create(path) 579 | writeLines(schema, file.path(path, "schema.json")) 580 | 581 | v <- json_validator(file.path(path, "schema.json"), engine = "ajv") 582 | expect_false(v("{}")) 583 | expect_true(v("{hello: 'world'}")) 584 | }) 585 | 586 | 587 | test_that("Parent schema with URL ID works", { 588 | parent <- c( 589 | '{', 590 | ' "$id": "http://example.com/schemas/thing.json",', 591 | ' "type": "object",', 592 | ' "properties": {', 593 | ' "hello": {', 594 | ' "$ref": "child.json#/definitions/greeting"', 595 | ' }', 596 | ' },', 597 | ' "required": ["hello"],', 598 | ' "additionalProperties": false', 599 | '}') 600 | child <- c( 601 | '{', 602 | ' "definitions": {', 603 | ' "first": {', 604 | ' "type": "string"', 605 | ' },', 606 | ' "greeting": {', 607 | ' "type": "object",', 608 | ' "properties": {', 609 | ' "name": {', 610 | ' "type": "string"', 611 | ' },', 612 | ' "another_prop": {', 613 | ' "type": "number"', 614 | ' }', 615 | ' }', 616 | ' }', 617 | ' }', 618 | '}') 619 | path <- tempfile() 620 | dir.create(path) 621 | writeLines(parent, file.path(path, "parent.json")) 622 | writeLines(child, file.path(path, "child.json")) 623 | 624 | v <- json_validator(file.path(path, "parent.json"), engine = "ajv") 625 | expect_false(v("{}")) 626 | expect_true(v('{"hello": {"name": "a name", "another_prop": 2}}')) 627 | }) 628 | 629 | test_that("format keyword works", { 630 | str <- '{ 631 | "type": "object", 632 | "required": ["date"], 633 | "properties": { 634 | "date": { 635 | "type": "string", 636 | "format": "date-time" 637 | } 638 | } 639 | }' 640 | v <- json_validator(str, "ajv") 641 | expect_false(v("{'date': '123'}")) 642 | expect_true(v("{'date': '2018-11-13T20:20:39+00:00'}")) 643 | }) 644 | 645 | test_that("format keyword works in draft-04", { 646 | str <- '{ 647 | "$schema": "http://json-schema.org/draft-04/schema#", 648 | "type": "object", 649 | "required": ["date"], 650 | "properties": { 651 | "date": { 652 | "type": "string", 653 | "format": "date-time" 654 | } 655 | } 656 | }' 657 | v <- json_validator(str, "ajv", strict = TRUE) 658 | expect_false(v("{'date': '123'}")) 659 | expect_true(v("{'date': '2018-11-13T20:20:39+00:00'}")) 660 | }) 661 | 662 | test_that("unknown format type throws an error if in strict mode", { 663 | str <- '{ 664 | "type": "object", 665 | "required": ["date"], 666 | "properties": { 667 | "date": { 668 | "type": "string", 669 | "format": "test" 670 | } 671 | } 672 | }' 673 | expect_error(json_validator(str, "ajv", strict = TRUE), 674 | paste0('Error: unknown format "test" ignored in schema at ', 675 | 'path "#/properties/date"')) 676 | 677 | ## Warnings printed in non-strict mode; these include some annoying 678 | ## newlines from the V8 engine, so using capture.output to stop 679 | ## these messing up testthat output 680 | capture.output( 681 | msg <- capture_warnings(v <- json_validator(str, "ajv", strict = FALSE))) 682 | expect_equal(msg[1], paste0('unknown format "test" ignored in ', 683 | 'schema at path "#/properties/date"')) 684 | expect_true(v("{'date': '123'}")) 685 | }) 686 | 687 | test_that("json_validate can be run in strict mode", { 688 | schema <- "{ 689 | '$schema': 'http://json-schema.org/draft-04/schema#', 690 | 'type': 'object', 691 | 'properties': { 692 | 'a': { 693 | 'const': 'foo' 694 | } 695 | }, 696 | 'reference': '1234' 697 | }" 698 | 699 | expect_true(json_validate("{'a': 'foo'}", schema, engine = "ajv")) 700 | 701 | expect_error( 702 | json_validate("{'a': 'foo'}", schema, engine = "ajv", strict = TRUE), 703 | 'Error: strict mode: unknown keyword: "reference"') 704 | }) 705 | 706 | 707 | test_that("validation works with 2019-09 schema version", { 708 | schema <- "{ 709 | '$schema': 'http://json-schema.org/draft-07/schema#', 710 | '$defs': { 711 | 'toggle': { 712 | '$id': '#toggle', 713 | 'type': [ 'boolean', 'null' ], 714 | 'default': null 715 | } 716 | }, 717 | 'type': 'object', 718 | 'properties': { 719 | 'enabled': { 720 | '$ref': '#toggle', 721 | 'default': true 722 | } 723 | } 724 | }" 725 | 726 | expect_true(json_validate("{'enabled': true}", schema, engine = "ajv")) 727 | expect_false(json_validate("{'enabled': 'test'}", schema, engine = "ajv")) 728 | 729 | ## Switch to draft/2019-09 730 | schema <- gsub("http://json-schema.org/draft-07/schema#", 731 | "https://json-schema.org/draft/2019-09/schema#", schema) 732 | ## draft/2019-09 doesn't allow #plain-name form of $id 733 | expect_error(json_validator(schema, engine = "ajv"), 734 | "Error: schema is invalid:") 735 | 736 | schema <- "{ 737 | '$schema': 'https://json-schema.org/draft/2019-09/schema#', 738 | '$defs': { 739 | 'toggle': { 740 | '$anchor': 'toggle', 741 | 'type': [ 'boolean', 'null' ], 742 | 'default': null 743 | } 744 | }, 745 | 'type': 'object', 746 | 'properties': { 747 | 'enabled': { 748 | '$ref': '#toggle', 749 | 'default': true 750 | } 751 | } 752 | }" 753 | expect_true(json_validate("{'enabled': true}", schema, engine = "ajv")) 754 | expect_false(json_validate("{'enabled': 'test'}", schema, engine = "ajv")) 755 | }) 756 | 757 | test_that("validation works with 2020-12 schema version", { 758 | schema <- "{ 759 | '$schema': 'https://json-schema.org/draft/2020-12/schema#', 760 | '$defs': { 761 | 'toggle': { 762 | '$anchor': 'toggle', 763 | 'type': [ 'boolean', 'null' ], 764 | 'default': null 765 | } 766 | }, 767 | 'type': 'object', 768 | 'properties': { 769 | 'enabled': { 770 | '$ref': '#toggle', 771 | 'default': true 772 | } 773 | } 774 | }" 775 | 776 | expect_true(json_validate("{'enabled': true}", schema, engine = "ajv")) 777 | expect_false(json_validate("{'enabled': 'test'}", schema, engine = "ajv")) 778 | }) 779 | 780 | 781 | test_that("ajv requires a valid meta schema version", { 782 | schema <- "{ 783 | '$schema': 'http://json-schema.org/draft-99/schema#', 784 | 'type': 'object', 785 | 'properties': { 786 | 'a': { 787 | 'const': 'foo' 788 | } 789 | } 790 | }" 791 | 792 | expect_error( 793 | json_validator(schema, engine = "ajv"), 794 | "Unknown meta schema version 'draft-99'") 795 | }) 796 | -------------------------------------------------------------------------------- /vignettes/jsonvalidate.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Introduction to jsonvalidate" 3 | author: "Rich FitzJohn" 4 | date: "`r Sys.Date()`" 5 | output: rmarkdown::html_vignette 6 | vignette: > 7 | %\VignetteIndexEntry{Introduction to jsonvalidate} 8 | %\VignetteEngine{knitr::rmarkdown} 9 | %\VignetteEncoding{UTF-8} 10 | --- 11 | 12 | ```{r echo = FALSE, results = "hide"} 13 | knitr::opts_chunk$set(error = FALSE) 14 | ``` 15 | 16 | This package wraps 17 | [is-my-json-valid](https://github.com/mafintosh/is-my-json-valid) 18 | using [V8](https://cran.r-project.org/package=V8) to do JSON schema 19 | validation in R. 20 | 21 | You need a JSON schema file; see 22 | [json-schema.org](https://json-schema.org/) for details on writing 23 | these. Often someone else has done the hard work of writing one 24 | for you, and you can just check that the JSON you are producing or 25 | consuming conforms to the schema. 26 | 27 | The examples below come from the [JSON schema 28 | website](https://json-schema.org//learn/getting-started-step-by-step.html) 29 | 30 | They describe a JSON based product catalogue, where each product 31 | has an id, a name, a price, and an optional set of tags. A JSON 32 | representation of a product is: 33 | 34 | ```json 35 | { 36 | "id": 1, 37 | "name": "A green door", 38 | "price": 12.50, 39 | "tags": ["home", "green"] 40 | } 41 | ``` 42 | 43 | The schema that they derive looks like this: 44 | 45 | ```json 46 | { 47 | "$schema": "http://json-schema.org/draft-04/schema#", 48 | "title": "Product", 49 | "description": "A product from Acme's catalog", 50 | "type": "object", 51 | "properties": { 52 | "id": { 53 | "description": "The unique identifier for a product", 54 | "type": "integer" 55 | }, 56 | "name": { 57 | "description": "Name of the product", 58 | "type": "string" 59 | }, 60 | "price": { 61 | "type": "number", 62 | "minimum": 0, 63 | "exclusiveMinimum": true 64 | }, 65 | "tags": { 66 | "type": "array", 67 | "items": { 68 | "type": "string" 69 | }, 70 | "minItems": 1, 71 | "uniqueItems": true 72 | } 73 | }, 74 | "required": ["id", "name", "price"] 75 | } 76 | ``` 77 | 78 | This ensures the types of all fields, enforces presence of `id`, 79 | `name` and `price`, checks that the price is not negative and 80 | checks that if present `tags` is a unique list of strings. 81 | 82 | There are two ways of passing the schema in to R; as a string or as 83 | a filename. If you have a large schema loading as a file will 84 | generally be easiest! Here's a string representing the schema 85 | (watch out for escaping quotes): 86 | 87 | ```{r} 88 | schema <- '{ 89 | "$schema": "http://json-schema.org/draft-04/schema#", 90 | "title": "Product", 91 | "description": "A product from Acme\'s catalog", 92 | "type": "object", 93 | "properties": { 94 | "id": { 95 | "description": "The unique identifier for a product", 96 | "type": "integer" 97 | }, 98 | "name": { 99 | "description": "Name of the product", 100 | "type": "string" 101 | }, 102 | "price": { 103 | "type": "number", 104 | "minimum": 0, 105 | "exclusiveMinimum": true 106 | }, 107 | "tags": { 108 | "type": "array", 109 | "items": { 110 | "type": "string" 111 | }, 112 | "minItems": 1, 113 | "uniqueItems": true 114 | } 115 | }, 116 | "required": ["id", "name", "price"] 117 | }' 118 | ``` 119 | 120 | Create a schema object, which can be used to validate a schema: 121 | 122 | ```{r} 123 | obj <- jsonvalidate::json_schema$new(schema) 124 | ``` 125 | 126 | If we'd saved the json to a file, this would work too: 127 | 128 | ```{r} 129 | path <- tempfile() 130 | writeLines(schema, path) 131 | obj <- jsonvalidate::json_schema$new(path) 132 | ``` 133 | 134 | ```{r include = FALSE} 135 | file.remove(path) 136 | ``` 137 | 138 | The returned object is a function that takes as its first argument 139 | a json string, or a filename of a json file. The empty list will 140 | fail validation because it does not contain any of the required fields: 141 | 142 | ```{r} 143 | obj$validate("{}") 144 | ``` 145 | 146 | To get more information on why the validation fails, add `verbose = TRUE`: 147 | 148 | ```{r} 149 | obj$validate("{}", verbose = TRUE) 150 | ``` 151 | 152 | The attribute "errors" is a data.frame and is present only when the 153 | json fails validation. The error messages come straight from 154 | `ajv` and they may not always be that informative. 155 | 156 | Alternatively, to throw an error if the json does not validate, add 157 | `error = TRUE` to the call: 158 | 159 | ```{r error = TRUE} 160 | obj$validate("{}", error = TRUE) 161 | ``` 162 | 163 | The JSON from the opening example works: 164 | 165 | ```{r} 166 | obj$validate('{ 167 | "id": 1, 168 | "name": "A green door", 169 | "price": 12.50, 170 | "tags": ["home", "green"] 171 | }') 172 | ``` 173 | 174 | But if we tried to enter a negative price it would fail: 175 | 176 | ```{r} 177 | obj$validate('{ 178 | "id": 1, 179 | "name": "A green door", 180 | "price": -1, 181 | "tags": ["home", "green"] 182 | }', verbose = TRUE) 183 | ``` 184 | 185 | ...or duplicate tags: 186 | 187 | ```{r} 188 | obj$validate('{ 189 | "id": 1, 190 | "name": "A green door", 191 | "price": 12.50, 192 | "tags": ["home", "home"] 193 | }', verbose = TRUE) 194 | ``` 195 | 196 | or just basically everything wrong: 197 | ```{r} 198 | obj$validate('{ 199 | "id": "identifier", 200 | "name": 1, 201 | "price": -1, 202 | "tags": ["home", "home", 1] 203 | }', verbose = TRUE) 204 | ``` 205 | 206 | The names comes from within the `ajv` source, and may be annoying to work with programmatically. 207 | 208 | There is also a simple interface where you take the schema and the 209 | json at the same time: 210 | 211 | ```{r} 212 | json <- '{ 213 | "id": 1, 214 | "name": "A green door", 215 | "price": 12.50, 216 | "tags": ["home", "green"] 217 | }' 218 | jsonvalidate::json_validate(json, schema, engine = "ajv") 219 | ``` 220 | 221 | However, this will be much slower than building the schema object once and using it repeatedly. 222 | 223 | Prior to 1.4.0, the recommended way of building a reusable validator object was to use `jsonvalidate::json_validator`; this is still supported but note that it has different defaults to `jsonvalidate::json_schema` (using imjv for backward compatibility). 224 | 225 | ```{r} 226 | v <- jsonvalidate::json_validator(schema, engine = "ajv") 227 | v(json) 228 | ``` 229 | 230 | While we do not intend on removing this old interface, new code should prefer both `jsonvalidate::json_schema` and the `ajv` engine. 231 | 232 | ## Combining schemas 233 | 234 | You can combine schemas with `ajv` engine. You can reference definitions within one schema 235 | 236 | ```{r} 237 | schema <- '{ 238 | "$schema": "http://json-schema.org/draft-04/schema#", 239 | "definitions": { 240 | "city": { "type": "string" } 241 | }, 242 | "type": "object", 243 | "properties": { 244 | "city": { "$ref": "#/definitions/city" } 245 | } 246 | }' 247 | json <- '{ 248 | "city": "Firenze" 249 | }' 250 | jsonvalidate::json_validate(json, schema, engine = "ajv") 251 | ``` 252 | You can reference schema from other files 253 | 254 | ```{r} 255 | city_schema <- '{ 256 | "$schema": "http://json-schema.org/draft-07/schema", 257 | "type": "string", 258 | "enum": ["Firenze"] 259 | }' 260 | address_schema <- '{ 261 | "$schema": "http://json-schema.org/draft-07/schema", 262 | "type":"object", 263 | "properties": { 264 | "city": { "$ref": "city.json" } 265 | } 266 | }' 267 | 268 | path <- tempfile() 269 | dir.create(path) 270 | address_path <- file.path(path, "address.json") 271 | city_path <- file.path(path, "city.json") 272 | writeLines(address_schema, address_path) 273 | writeLines(city_schema, city_path) 274 | jsonvalidate::json_validate(json, address_path, engine = "ajv") 275 | ``` 276 | 277 | You can combine schemas in subdirectories. Note that the `$ref` path needs to be relative to the schema path. You cannot use absolute paths in `$ref` and jsonvalidate will throw an error if you try to do so. 278 | 279 | ```{r} 280 | user_schema = '{ 281 | "$schema": "http://json-schema.org/draft-07/schema", 282 | "type": "object", 283 | "required": ["address"], 284 | "properties": { 285 | "address": { 286 | "$ref": "sub/address.json" 287 | } 288 | } 289 | }' 290 | 291 | json <- '{ 292 | "address": { 293 | "city": "Firenze" 294 | } 295 | }' 296 | 297 | path <- tempfile() 298 | subdir <- file.path(path, "sub") 299 | dir.create(subdir, showWarnings = FALSE, recursive = TRUE) 300 | city_path <- file.path(subdir, "city.json") 301 | address_path <- file.path(subdir, "address.json") 302 | user_path <- file.path(path, "schema.json") 303 | writeLines(city_schema, city_path) 304 | writeLines(address_schema, address_path) 305 | writeLines(user_schema, user_path) 306 | jsonvalidate::json_validate(json, user_path, engine = "ajv") 307 | ``` 308 | --------------------------------------------------------------------------------