אימות JSON ב-CI: צינור עם jsonlint, ajv ו-jq
שורה תחתונה: אימות תחביר מוכיח שקובץ נפרסר; אימות סכימה מוכיח שהשדות עומדים בחוזה. ב-CI, מה שחשוב הוא קודי יציאה חד-משמעיים, מיקומי שגיאה ונתיבי תיקון.

שורה תחתונה: ב-CI, תנו ל-jq empty לטפל בתחביר, ל-ajv-cli במבנה (check-jsonschema לצוותים מרובי-פורמטים), ול-Prettier בסגנון; שרשרו אותם לפי שכבות ועצרו בכשל הראשון.
מעבר של המפרסר בעורך המקומי שלכם לא אומר שהקובץ שייך ל-CI; מעבר של CI לא אומר שהשדות מתאימים לחוזה.
1. הפרידו בין אימות תחביר לאימות סכימה
הביטוי "אימות JSON" משמש באופן רחב מדי, ולכן צוותים בוחרים לעתים קרובות כלי אחד כדי לפתור שתי בעיות שונות:
| שכבה | השאלה הנענית | כלים טיפוסיים | מה משמעות כשל |
|---|---|---|---|
| תחביר | האם הקובץ הוא JSON תקין? | jq empty, jsonlint, python -m json.tool |
סוגריים, פסיקים, מרכאות או קידוד שבורים |
| סכימה | האם שדות, טיפוסים ומאפיינים נדרשים מתאימים לחוזה? | ajv-cli, check-jsonschema |
הקובץ נפרסר, אבל המבנה העסקי שגוי |
| סגנון | האם ההזחה וסדר המפתחות עקביים? | Prettier, jq -S |
הפורמט לא עקבי וה-diff-ים רועשים |
השכבות האלה לא יכולות להחליף זו את זו. קובץ שעובר תחביר יכול עדיין להיכשל בסכימה: {"price": "12.5"} הוא JSON תקין, אבל אם הסכימה דורשת ש-price יהיה מספר, הוא אינו מקובל. לעומת זאת, לא משנה כמה הסכימה שלמה, קובץ שחסר בו סוגר לא יתקמפל מולה לעולם.
לכן הצינור צריך להריץ תחביר קודם, מבנה שני וסגנון אחרון. עצרו ברגע ששכבה נכשלת, אחרת שגיאת פרסור מתחזה לשגיאת שדה.
2. שכבת תחביר: jq empty היא אפשרות התלות המינימלית
אם jq כבר נמצא בתמונת ה-CI, אימות תחביר כמעט לא עולה דבר:
jq empty data.json
jq empty אינו מייצר פלט; הוא רק מפרסר את הקובץ. הוא יוצא עם 0 בהצלחה, ועם קוד לא-אפס בתוספת מידע שורה/עמודה בכשל. זה הופך אותו לקל יותר מאשר למשוך עץ תלויות Node מלא, ולשער ראשון טוב.
באימות אצווה של קובצי JSON במאגר, החריגו את node_modules, פלט build ומטמוני צד שלישי:
find . -name '*.json' \
-not -path './node_modules/*' \
-not -path './dist/*' \
-print0 | xargs -0 -n1 jq empty
-print0 ו-xargs -0 מטפלים ברווחים בשמות קבצים, בעוד ש--n1 מבטיח קובץ אחד לכל קריאה כך שהודעת השגיאה תזהה את האשם. אל תעבירו מאות קבצים לתהליך יחיד; הודעות השגיאה ידרסו זו את זו.
jsonlint הוא אפשרות נפוצה נוספת, שבדרך כלל מופעלת כ-npx jsonlint -q data.json, כאשר -q מדפיס רק שגיאות. הוא מדווח מספרי שורה ועמודה, אבל התחזוקה שלו פחות פעילה מ-jq, וגרסאות שונות סובלניות ל-BOM ולקומנטים בצורה שונה. התייחסו אליו כבודק תחביר קריא יותר, לא כמאמת סכימה.
3. שכבת סכימה: יסודות ajv-cli validate
Ajv הוא מאמת הסכימות הנפוץ ביותר באקוסיסטם של JavaScript, ו-ajv-cli חושף אותו ככלי שורת פקודה:
ajv validate -s schema.json -d data.json
-s מצביע על קובץ הסכימה ו--d על הנתונים שמאומתים. בכשל התהליך יוצא לא-אפס, כך ש-CI נכשל מיד. פרויקטים אמיתיים בדרך כלל מוסיפים שני דגלים:
npx ajv-cli validate \
-s schemas/order.json \
-d 'fixtures/orders/*.json' \
--strict=false \
--errors=text
--strict=false עוזר לסכימות ישנות: מצב קפדן מתייחס למילות מפתח לא מוכרות ולהצהרות required חשודות כשגיאות, ורבות מסכימות legacy נכשלות בו. --errors=text מייצר פלט קריא לאדם, בעוד שמבנה ה-JSON כברירת מחדל טוב יותר למכונות. בחרו לפי מי שצורך את התוצאה.
גם גרסת ה-draft של הסכימה צריכה להתאים. גרסאות ראשיות של Ajv תומכות כברירת מחדל ב-draft-ים שונים, ו-ajv-cli יכול לבחור אחד עם --spec=draft7, --spec=draft2019 או --spec=draft2020. אם סכימה מצהירה $schema: "https://json-schema.org/draft/2020-12/schema" אבל ה-CLI מקמפל אותה כ-draft-07, חלק ממילות המפתח ייתעלמו בשקט. למידע נוסף על כתיבת סכימות והבדלי גרסאות, ראו תחילת העבודה עם JSON Schema.
אימות פורמט הוא דרישה נפוצה נוספת. מילת המפתח format היא הערה בלבד כברירת מחדל; כדי ש-email, date-time ו-uri יאכפו משהו בפועל, טענו את ajv-formats:
npx ajv-cli validate -c ajv-formats -s schema.json -d data.json
קל לפספס את הצעד הזה, והתוצאה היא ששדה email מקבל not-an-email. לפני שחרור, האכילו את הצינור ברשומה אחת לא תקינה בכוונה; זה מוכיח שהחוק פעיל הרבה יותר טוב מקריאת התיעוד עשר פעמים.
check-jsonschema: כמה פורמטים ושגיאות ידידותיות יותר
אם הצוות מטפל ב-YAML, TOML או הגדרות GitHub Actions בנוסף ל-JSON, check-jsonschema נוח יותר:
check-jsonschema --schemafile schemas/config.json config/app.json
check-jsonschema --schemafile schemas/pipeline.json .github/workflows/ci.yml
היתרונות המעשיים שלו:
- תמיכה מקורית ב-YAML, שמתאימה לקבצים שהם "בצורת JSON אבל כתובים ב-YAML", כמו מניפסטים של Kubernetes והגדרות CI.
- שגיאות כוללות את נתיב הקובץ, מספר השורה ונתיב השדה, ושומרות על עלות איתור נמוכה.
- מסופק ווי pre-commit, כך שהשילוב זול.
- הוא יכול למשוך סכימות נפוצות מ-SchemaStore, אבל גם מקבל קבצים מקומיים, מה שמונע תלות ברשת.
בצוותים חוצי-שפות, Python בדרך כלל כבר נמצא, ולכן התקנה ושדרוג של check-jsonschema קלים יותר מתחזוקת שרשרת כלים של Node. תנו ל-jq לטפל בתחביר ולכלי הזה במבנה; השילוב בעל תקורת תחזוקה נמוכה מאוד.
4. קודי יציאה הם האות היחיד שאפשר לסמוך עליו ב-CI
לא משנה כמה מהודר הפלט, CI קורא רק קודי יציאה. שלוש מלכודות נפוצות:
| סימפטום | סיבה | תיקון |
|---|---|---|
| יש שגיאות אבל CI ירוק | הפקודה מסתיימת ב-` | |
| אין שגיאות אבל CI אדום | diff מתייחס ל"נמצאו הבדלים" ככשל |
הבדילו בין "הבדל" ל"שגיאת הרצה" |
| פלט השגיאה נחתך | פלט מקבילי של כמה קבצים דורס את עצמו | הריצו סדרתית או הדפיסו לפי קובץ |
זכרו את סמנטיקת קודי היציאה של jq: פרסור רגיל מחזיר 0; עם -e, תוצאה של false או null מחזירה 1; שגיאות שימוש וקבצים חסרים מחזירים 2; שגיאות קומפילציה של סקריפט מחזירות 3. כלי סכימה בדרך כלל משתמשים ב-0 להצלחה, 1 לכשל אימות ו-2 לשגיאות כלי או ארגומנטים. כשכותבים עטיפה, אל תכווצו כל קוד לא-אפס ל"הנתונים שגויים", אחרת כשל סביבה יאובחן בטעות כבעיית תוכן.
5. pre-commit: לתפוס בעיות לפני ה-commit
הערך של pre-commit אינו "עוד בדיקה"; הוא לתת למפתחים משוב בתוך דקה במקום אחרי ריצת CI של עשר דקות. הגדרה מינימלית נראית כך:
repos:
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.33.0
hooks:
- id: check-jsonschema
files: ^configs/.*\.json$
args:
- --schemafile
- schemas/config.json
- repo: local
hooks:
- id: jq-syntax
name: jq syntax check
entry: jq empty
language: system
files: ^configs/.*\.json$
שני פרטים חשובים:
filesצריך להתאים רק לספריות שדורשות אימות. אל תריצו ווי עלnode_modulesאו קבצים מיוצרים.- הווי המקומי מניח ש-
jqמותקן במכונת המפתח. תעדו את הדרישה, או עברו לווי קונטיינרי.
אם הפרויקט כבר משתמש ב-Husky וב-lint-staged, שלבו אותם ישירות:
{
"lint-staged": {
"configs/*.json": ["jq empty", "prettier --check"]
}
}
זכרו ש-lint-staged מעביר רשימת קבצים ב-staging, ולכן הפקודה חייבת לקבל כמה נתיבים. גם jq empty וגם prettier --check עושים זאת. אל תתנו לווי לסרוק את כל המאגר; ה-commit-ים יאטו בצורה מורגשת.
6. GitHub Actions: צינור שאפשר להעתיק
ה-workflow הבא מבצע אימות תחביר, אימות סכימה ושומר את הדוח בכשל:
name: json-validate
on:
push:
paths:
- 'schemas/**'
- 'fixtures/**'
pull_request:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Syntax check
run: |
find fixtures -name '*.json' -print0 | xargs -0 -n1 jq empty
- name: Schema check
run: |
npx --yes ajv-cli validate \
-s schemas/order.json \
-d 'fixtures/orders/*.json' \
--strict=false \
--errors=text
מסנן ה-paths מונע משינויים בתיעוד בלבד להפעיל אימות. npx --yes מתאים למאגרים בלי תלויות מוצמדות; פרויקטי production צריכים להוסיף את ajv-cli ל-devDependencies ולהצמיד גרסה. בלי lock, שחרור במעלה הזרם שמשנה את ה-draft או מצב ה-strict כברירת מחדל יכול להפוך את הצינור לאדום בין לילה.
7. איתור שגיאה: ממספרי שורות לנתיבי שדות
חצי מהערך של מאמת הוא זיהוי; החצי השני הוא איתור. למדו לקרוא שלושה סוגי פלט:
- שגיאות תחביר:
jqמדווח שורה ועמודה, לעתים עם קטע סמוך. תקנו קודם סוגריים ופסיקים; אל תחשדו מיד בקידוד. - שגיאות סכימה:
instancePathשל Ajv מצביע על מיקום הנתונים (למשל/items/0/price),schemaPathמצביע על החוק (למשל#/properties/items/items/properties/price/type), ו-keywordנוקב במגבלה שהופרה. - שגיאות פורמט: כשבדיקת
formatנכשלת, ההודעה לעתים רק אומרת "must match format "email"". שלבו אותה עםinstancePathכדי לזהות את השדה.
הדביקו את instancePath לתיעוד החקירה; הוא שימושי הרבה יותר מקיר JSON. במערכים, האינדקס המספרי גם אומר איזו רשומה נכשלה. אם המפתח העסקי אינטואיטיבי יותר, הוסיפו אחר כך שורת מיפוי, כמו items[0].id = ORD-042.
טעויות סכימה נפוצות, כמו שימוש יתר ב-additionalProperties: false או כתיבת ענפי oneOf שחופפים בפועל, מכוסות ב-מלכודות נפוצות ביצירת JSON Schema. חוק אימות שגוי מסוכן יותר מאי-אימות, כי הוא יוצר תחושת ביטחון כוזבת.
8. טבלת בחירת כלים
| צורך | כלי מומלץ | למה | מה להיזהר |
|---|---|---|---|
| תחביר בלבד | jq empty |
אפס תלויות, מהיר, קודי יציאה ברורים | רווחים בשמות קבצים בהרצת אצווה |
| שגיאות שורה/עמודה ידידותיות יותר | jsonlint |
פלט קריא | פעילות התחזוקה מוגבלת |
| אימות JSON Schema | ajv-cli |
אקוסיסטם בוגר, שגיאות מובנות | יישור גרסאות draft וטעינת תמיכת format |
| מאגר מעורב JSON ו-YAML | check-jsonschema |
ריבוי פורמטים, ידידותי ל-pre-commit | מקור הסכימה וגישה לרשת |
| משוב מהיר לפני commit | pre-commit / lint-staged | מעבד רק קבצים ב-staging | הווי חייב לקבל כמה נתיבים |
| פורמט עקבי | Prettier | אימוץ רחב | השתמשו ב---check כשלא רוצים לכתוב קבצים מחדש |
המלצה אחרונה: ודאו שהצינור באמת נכשל בכך שתאכילו אותו קודם ברשומה אחת לא תקינה בכוונה. הסיכון הגדול אינו הגדרה מסובכת; הוא הגדרה שלעולם אינה נכנסת לתוקף. התייחסות ל"הוא יכול להיכשל" כקריטריון קבלה אמינה יותר מאשר התייחסות ל"הוא עובר". לשרשרת כלי שורת הפקודה הרחבה יותר, המשיכו ל-עורכים, שורת פקודה ואוטומציה.
FAQ
כדאי להשתמש ב-jq empty או ב-jsonlint?
העדיפו jq empty ב-CI: אפס תלויות, התחלה מהירה וסמנטיקת קודי יציאה ברורה. השתמשו ב-jsonlint כשצריך הודעות שורה/עמודה ידידותיות יותר או כשחוקרים מקומית. שניהם מבצעים אימות תחביר בלבד; אף אחד מהם לא בודק מבנה שדות.
למה ajv מדווח על מילת מפתח לא מוכרת?
ודאו קודם שה-draft של $schema בסכימה מתאים לגרסה שה-CLI משתמש בה. רבות משגיאות "מילת מפתח לא מוכרת" הן מילות מפתח של draft חדש שמקומפל ישן נתקל בהן. אם הגרסאות מתאימות, בדקו שגיאת כתיב. --strict=false יכול לשמור על תאימות סכימות legacy, אבל אל תסתמכו עליו לטווח ארוך כדי להסתיר בעיות אמיתיות.
אימות הסכימה עובר, אבל ה-API עדיין נכשל. למה?
סכימה מאמתת מבנה, לא סמנטיקה עסקית. למשל, status יכול להיות מחרוזת עם enum חוקי, בעוד שמכונת המצב אוסרת מעבר ישיר מ-paid ל-refunded. חוקים כאלה שייכים לקוד או לאילוצי בסיס נתונים, לא ל-JSON Schema.
איך מאתרים במהירות את השדה שנכשל?
קראו את instancePath או מספר השורה מפלט הכלי. instancePath של Ajv הוא JSON Pointer שמצביע ישירות על הנתונים; check-jsonschema מדווח קובץ ושורה. תעדו את הנתיב ומפו אותו למפתח העסקי; איתור הבעיה בדרך כלל לוקח דקות.
האם pre-commit צריך להריץ אימות מלא?
לא. pre-commit צריך לעבד רק קבצים ב-staging כדי לשמור על מהירות ה-commit. סריקות מלאות שייכות ל-CI או למשימות מתוזמנות. החלוקה פשוטה: ריצות מקומיות מעדיפות מהירות, CI מעדיף שלמות.