-
Notifications
You must be signed in to change notification settings - Fork 3.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[amp story shopping] JSON schema #37746
Comments
I propose that instead of keeping the JSON schema in sync with the implementation code, we generate the code from schema instead. We can use |
@jshamble when you have bandwidth, can you try writing a JSON schema for the config JSON? I think it should be fairly straightforward, given that the structure isn't very verbose. Once we have a test schema based on #36460, it should be pretty easy to see the bundle size of the output concretely, which I know is a concern that @gmajoulet brought up. I'd love to do this if we can keep bundle size small; thanks @alanorozco for investigating! |
Actually, I forgot I'd experimented with writing a JSON schema spec to represent an entire story (doesn't quite have feature parity, but it's a reasonable representation): https://newmuis.github.io/story-schema/ I have a make-dir dist && ajv compile -s 'root.schema.json' -r 'schemas/**/*.schema.json' -o 'dist/story-validator.js' --strict --all-errors=true --messages=false --inline-refs=false --code-optimize=10 With the full story schema, it gets: $ yarn run compile-schema && gzip ./dist/story-validator.js && wc -c < ./dist/story-validator.js.gz
16723 Then I tried simplifying the schema to: {
"$id": "https://github.com/newmuis/story-schema/root.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object"
} This is one of the simplest schemas you can have AFAIK, and it yields: $ yarn run compile-schema && gzip ./dist/story-validator.js && wc -c < ./dist/story-validator.js.gz
495 I think this bounds the gzipped, minified output between |
tl;dr
@alanorozco @calebcordry @gmajoulet for your thoughts on the bundle size Actually, while procrastinating from... other things... I just threw together a quick schema. @jshamble and @processprocess's outline in #36460 made it pretty easy, and I followed it pretty closely: {
"$id": "https://github.com/newmuis/story-schema/test.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": [
"items"
],
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"productUrl",
"productId",
"productBrand",
"productTitle",
"productPrice",
"productPriceCurrency",
"productImages",
"productDetails",
"aggregateRating"
],
"properties": {
"productUrl": { "type": "string" },
"productId": { "type": "string" },
"productBrand": { "type": "string" },
"productIcon": { "type": "string" },
"productTitle": { "type": "string" },
"productDetails": { "type": "string" },
"productPrice": { "type": "number", "minimum": 0 },
"productPriceCurrency": {
"enum": [
"AED",
"AFN",
/* like, 150+ more currencies */
"ZMW",
"ZWL"
]
},
"productImages": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": [ "url", "altText" ],
"properties": {
"url": { "type": "string" },
"altText": { "type": "string" }
}
}
},
"aggregateRating": {
"type": "object",
"additionalProperties": false,
"required": [ "ratingValue", "ratingCount", "ratingUrl" ],
"properties": {
"ratingValue": { "type": "number", "minimum": 0 },
"ratingCount": { "type": "number", "minimum": 0 },
"ratingUrl": { "type": "string" }
}
}
}
}
}
}
}
Then I re-ran the same methodology as above: $ yarn run compile-schema && gzip ./dist/story-validator.js && wc -c < ./dist/story-validator.js.gz
3464 So, this is 3.3kB gzipped. Of course, as the schema itself grows, as will the bundle size. Again, I should caveat that I haveno idea whether I'm compiling this with the most efficient options. I then also took just the validation piece of @jshamble's #37474, and added to it the currency validation (the big list of ~180 currencies), to give me this: const productImagesValidation = {
'url': [validateRequired, validateURLs],
'alt': [validateRequired, validateString],
};
const aggregateRatingValidation = {
'ratingValue': [validateRequired, validateNumber],
'reviewCount': [validateRequired, validateNumber],
'reviewUrl': [validateRequired, validateURLs],
};
export const productValidationConfig = {
/* Required Attrs */
'productUrl': [validateRequired, validateURLs],
'productId': [validateRequired, validateString],
'productTitle': [validateRequired, validateString],
'productBrand': [validateRequired, validateString],
'productPrice': [validateRequired, validateNumber],
'productImages': [
validateRequired,
createValidateConfigArray(productImagesValidation),
],
'productPriceCurrency': [validateRequired, validateString, validateCurrency],
'aggregateRating': [
validateRequired,
createValidateConfig(aggregateRatingValidation),
],
/* Optional Attrs */
'productIcon': [validateURLs],
'productTagText': [validateString],
'ctaText': [validateNumber],
'shippingText': [validateNumber],
};
/**
* @typedef {{
* items: !Array<!ShoppingConfigDataDef>,
* }}
*/
let ShoppingConfigResponseDef;
/**
* Used for keeping track of intermediary invalid config results within Objects or Arrays of Objects.
* @private {boolean}
*/
let isValidConfigSection_ = true;
/**
* Validates an Object using the validateConfig function.
* @param {?Object=} validation
* @return {boolean}
*/
function createValidateConfig(validation) {
return (field, value) => {
if (!isValidConfigSection_) return;
isValidConfigSection_ = validateConfig(value, validation, field + ' ');
};
}
/**
* Validates an Array of Objects using the validateConfig function.
* @param {?Object=} validation
* @return {boolean}
*/
function createValidateConfigArray(validation) {
return (field, value) => {
for (const item of value) {
if (!isValidConfigSection_) return;
isValidConfigSection_ = validateConfig(item, validation, field + ' ');
}
};
}
/**
* Validates if a required field exists for shopping config attributes
* @param {string} field
* @param {?string=} value
*/
export function validateRequired(field, value) {
if (value === undefined) {
throw Error(`Field ${field} is required.`);
}
}
/**
* Validates if string type for shopping config attributes
* @param {string} field
* @param {?string=} str
*/
export function validateString(field, str) {
if (typeof str !== 'string') {
throw Error(`${field} ${str} is not a string.`);
}
}
/**
* Validates number in shopping config attributes
* @param {string} field
* @param {?number=} number
*/
export function validateNumber(field, number) {
if (
(typeof number === 'string' && !/^[0-9.,]+$/.test(number)) ||
(typeof number !== 'string' && typeof number !== 'number')
) {
throw Error(`Value ${number} for field ${field} is not a number`);
}
}
export function validateCurrency(field, str) {
return [
"AED",
"AFN",
/* like, 150+ more currencies */
"ZMW",
"ZWL"
].includes(str);
}
/**
* Validates url of shopping config attributes
* @param {string} field
* @param {?Array<string>=} url
*/
export function validateURLs(field, url) {
if (url === undefined) {
return;
}
const urls = Array.isArray(url) ? url : [url];
urls.forEach((url) => {
assertHttpsUrl(url, `amp-story-shopping-config ${field}`);
});
}
/**
* Validates the shopping config of a single product.
* @param {!ShoppingConfigDataDef} shoppingConfig
* @param {!Object<string, !Array<function>>} validationObject
* @param {?string} optParentFieldName
* @return {boolean}
*/
export function validateConfig(
shoppingConfig,
validationObject = productValidationConfig,
optParentFieldName = ''
) {
let isValidConfig = true;
Object.keys(validationObject).forEach((configKey) => {
const validationFunctions = validationObject[configKey];
validationFunctions.forEach((fn) => {
try {
/* This check skips optional attribute validation */
if (
shoppingConfig[configKey] !== undefined ||
validationFunctions.includes(validateRequired)
) {
fn(configKey, shoppingConfig[configKey]);
}
} catch (err) {
isValidConfig = false;
user().warn('AMP-STORY-SHOPPING-CONFIG', `${optParentFieldName}${err}`);
}
});
});
return isValidConfig;
} When I minify and gzip this (excluding the core AMP dependencies like
|
Thanks for the awesome analysis @newmuis ! It seems like the main question is if the extra 2.1 kB is worth the maintenance cost of manually keeping the schema in sync? I really like the idea of JSON Schema but unfortunately I think the 2.1kB might be too large of a price to pay (unless, as Jon mentioned, we might be missing some compiler optimizations). I believe @gmajoulet has data showing real impact on users making it to "story load" for a magnitude of ~3kB. |
About bundle size, as long as it is NOT in the critical loading path (e.g. v0/amp-story/amp-analytics...) it's acceptable. |
Other components use regular AMP validation, do I think they get coverage that we won't here. The only story features I can think of that use JSON thus far (ads, analytics, consent) do not change the narrative and can likely have a single setup per-tool, just plugging in a value or two. This has many dimensions of configurability across tool, shopping vendor, and creator. It probably benefits us to have a clear spec and a canonical validation flow so folks can make sure their getting it right |
My main point is that this has a strict negative impact on end users, just so we don't have to maintain and keep in sync custom validation + schema. DevX is great, but users experience should go first. Making the shopping bundle slightly heavier is fine as it is lazy loaded after the story is rendered, but that means we couldn't use it for anything part of the main bundles (as @newmuis mentioned, Ads, analytics, consent). However I'm not going to be resistant to this change, as long as it doesn't impact the main bundles. |
Got it. I think all I care about is that we have a distributable means of validation, but how we actually validate against it doesn't matter as much to me. But it's actually quite important that we're sure that the validation used by tools and the validation used at runtime work the same way (down to the little nuances) -- as long as we can guarantee that, we're good 🙂 |
@newmuis For the validate currency function, I am in favor of using javascript's built in
|
I ran some tests compiling my JSON validator with this minifier For Getting 3.4 KB after gzip compression. As I mentioned in the comment right above this one, using built in javascript I cannot predict what fields we will add in the future, but what I can say is that there would potentially be more edge cases (like the validate currency field) that we can easily handle with built in JS functions. One good example I can think of is the I would make the same argument as above, rather than listing the valid timezones in a schema (i.e. UTC,PST, etc.) and adding on more kb, why not just use the already built-in js functions and rely on their error handling? |
It's a tradeoff, I think. This is undoubtedly easier and lighter when we only concern ourselves with JavaScript stacks. But, if you imagine a stories creation tool written in anything other than JS (of which there are several.... It might actually be the majority of tools), they would then need to understand how to emulate the behavior of these JavaScript functions. The nice thing about JSON schema is that it is guaranteed to be self-contained and has tooling support across many languages. It seems reasonably painful for tools to need to, on a field-by-field basis, understand JavaScript currency handling, date parsing, etc, especially given that the differences are often quite nuanced. |
To parse the core goals from this discussion to help guide our decision: To achieve 1: To achieve 2: Unless we believe there are strong use-cases for shopping on the first page (like single page stories), JSON schema + ajv may be the most scalable and maintainable approach. |
After a conversation with @newmuis, for the shopping launch, we have decided to move forward with both solutions, i.e. JSON schema + ajv and JS client-side validation. The main reason is that: What if validation on the tooling level fails? |
We might not need to take the the size tradeoff of using
With some remapping, we can generate the ideal |
Is the new schema at https://github.com/ampproject/amphtml/blob/main/examples/amp-story/shopping/product.schema.json also accessible via some URL at amp.dev or so? Or are tools expected to grab it from GitHub for now? Also, does this schema adhere to a specific version of JSON schema? There's no |
I don't think there currently is a URL at amp.dev which hosts the
without a specified VSCode does not support anything after draft-2020-12 yet, but we can still specify the |
Description
A JSON schema to spec the API for the shopping config and use for validation within tooling.
This file could be hosted on GitHub as part of the project but we do not want to include it in our bundle.
We will need to keep it in-sync with any config changes.
cc @calebcordry
Alternatives Considered
We could point a link to the validation javascript file, but that would require people to read through the component code.
JSON schema is scalable and standardized which will help ease of integration into tooling.
Additional Context
More context within discussion on shopping validation I2I.
The text was updated successfully, but these errors were encountered: