Skip to content

Commit 33f286a

Browse files
authored
Merge pull request #55 from ropensci/safe-serialise
2 parents 4d9f6dc + 8f2c979 commit 33f286a

18 files changed

+924
-119
lines changed

DESCRIPTION

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: jsonvalidate
22
Title: Validate 'JSON' Schema
3-
Version: 1.3.2
3+
Version: 1.4.0
44
Authors@R: c(person("Rich", "FitzJohn", role = c("aut", "cre"),
55
email = "[email protected]"),
66
person("Rob", "Ashton", role = "aut"),
@@ -21,6 +21,7 @@ URL: https://docs.ropensci.org/jsonvalidate/,
2121
https://github.com/ropensci/jsonvalidate
2222
BugReports: https://github.com/ropensci/jsonvalidate/issues
2323
Imports:
24+
R6,
2425
V8
2526
Suggests:
2627
knitr,

NAMESPACE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# Generated by roxygen2: do not edit by hand
22

3+
export(json_schema)
4+
export(json_serialise)
35
export(json_validate)
46
export(json_validator)

NEWS.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
1-
## jsonvalidate 1.3.2
1+
# jsonvalidate 1.4.0
2+
3+
* Support for safely serialising objects to json, guided by the schema, with new function `json_serialise`
4+
* New object `json_schema` for construction of reusable validation and serialisation functions
5+
6+
# jsonvalidate 1.3.2
27

38
* Always uses ES5 version of Ajv, which allows use in both current and "legacy" V8 (#51)
49

5-
## jsonvalidate 1.3.0
10+
# jsonvalidate 1.3.0
611

712
* Upgrade to ajv version 8.5.0
813
* 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
914

10-
## jsonvalidate 1.2.3
15+
# jsonvalidate 1.2.3
1116

1217
* 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`
1318
* JSON can be validated against a subschema (#18, #19, @AliciaSchep)
1419
* Validation with `error = TRUE` now returns `TRUE` (not `NULL)` on success
1520
* Schemas can span multiple files, being included via `"$ref": "filename.json"` - supported with the ajv engine only (#20, #21, @r-ash).
1621
* Validation can be performed against a fraction of the input data (#25)
1722

18-
## jsonvalidate 1.1.0
23+
# jsonvalidate 1.1.0
1924

2025
* 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)
2126

22-
## jsonvalidate 1.0.1
27+
# jsonvalidate 1.0.1
2328

2429
* Initial CRAN release

R/schema.R

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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 supercedes [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+
}

R/serialise.R

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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 serialsation 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+
##' @section: 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+
}

0 commit comments

Comments
 (0)