How JSON Schemas Work

📒 Table of Contents

Overview

In this documentation, you will learn how JSON schemas work by following a real example that walks through the contract_details schema.

You’ll also learn how to create a UI form using that JSON Schema with a JavaScript library called json-schema-form that Remote created and uses internally on the Remote Platform.

Before you get started

Before you get started, ensure that you:

Creating a dynamic form with JSON schemas

A dynamic format for employment details using JSON Schema

The Remote API uses JSON Schema capabilities to handle the different formats required by a single endpoint depending on the country, employment, or other attribute of the payload.

The data Remote needs for onboarding employees and contractors changes over time due to the changes in government regulations, compliance rules, availability of benefit packages, etc. These changes aren’t always backwards compatible: Sometimes new regulations requires Remote to collect some data that was previously optional or some benefit package is now required to be offered by employers. Moreover these changes can happen on a weekly basis and they’re different for every country Remote supports.

Given the above, documenting all the conditionals, optional, and required fields for each country would be ineffective. Developers would have a series of problems finding the right combination of information to be provided. To overcome this problem, the Remote API was built to dynamically adapt to changes in employment data requirements, by utilizing JSON Schemas to represent the format of data developers need to send.

Fetching the employment data schemas

The update employment endpoint accepts a lot of employment-specific data in its request body. For this example, we will look at the contract_details JSON schema, as it’s one of the more complex schemas.

The API documentation for the update employment endpoint gives details on how to figure out what’s needed for these two fields. For example, for contract_details the documentation indicates the following:

Contract information. As its properties may vary depending on the country, you must query the Show form schema endpoint passing the country code and contract_details as path parameters.

Fetching the contract_details schema

Let’s look at the show form schema endpoint documentation. It indicates that we need to send a GET request to this endpoint, to get the desired JSON schema:

plain text
1https://gateway.remote-sandbox.com/v1/countries/{country_code}/{form}
ℹ️
When you’re ready to release your integration, replace the domain with https://gateway.remote.com You can find the API documentation for the /v1/countries/{country_code}/{form} endpoint here.

Let’s assume our employment is located in Canada. In that case, we have to use Canada’s country_code to make this request. The {form} will be contract_details, as indicated to us in the quote above we took from the update employment endpoint documentation.

If you don’t know the country_code for Canada, you can make a request to the list supported countries endpoint. You’ll see that the country_code for Canada is CAN.

We’re now ready to make our request:

bash
1$ curl --location \
2 --request GET 'https://gateway.remote-sandbox.com/v1/countries/CAN/contract_details' \
3 --header 'Authorization: Bearer eyJraWQiO...' \

Understanding the response for the contract_details schema

⚠️
The response you receive may be 1000+ lines for any given schema. For convenience, we’ve included the full example here, but you should always make a request to the show form schema endpoint to obtain the latest schemas, as they’re dynamic and will change over time.
(Click to expand) The entire response payload.
json
1{
2 "data": {
3 "schema": {
4 "additionalProperties": false,
5 "properties": {
6 "annual_gross_salary": {
7 "presentation": {
8 "currency": "CAD",
9 "inputType": "money"
10 },
11 "title": "Annual gross salary",
12 "type": "integer",
13 "x-jsf-errorMessage": {
14 "type": "Please, use US standard currency format. Ex: 1024.12"
15 }
16 },
17 "available_pto": {
18 "description": "We recommend at least 20 days. Note that employees are legally entitled to vacation pay calculated at 4% - 7% of all wages earned in the vacation entitlement year and a vacation pay calculated at 4-7% of all wages earned in the vacation entitlement year. Please note that Statutory, Bank Holidays and Public Holidays, based on employee's country of residence are excluded from the above.",
19 "presentation": {
20 "inputType": "number"
21 },
22 "title": "Number of paid time off days",
23 "type": "number"
24 },
25 "benefits": {
26 "additionalProperties": false,
27 "description": "Remote offers its employees supplemental, comprehensive coverage with Remote Health, provided in partnership with Canada Life and Lifeworks. This is to supplement the basic coverage. Check out <a href=\"https://remote.com/benefits-guide/canada\" target=\"_blank\" rel=\"nofollow noreferrer noopener\" class=\"sc-d712774a-4 ipXXBI\">our benefits guide ↗</a> for a full coverage comparison view.\r\n\r\nShould you want to offer any benefits not mentioned below, you may opt to gross up the salary to account for the costs of employee getting those benefits privately.",
28 "presentation": {
29 "extra": "The package you have selected will apply to any current and future employees in Canada.\r\n\r\nPricing note: Benefit contributions will be charged after the enrollment window closes. This means your first payment will include the previous and current month, based on your employees selections.\r\n\r\nPricing listed is subject to change based plan changes/renewals. Any pricing changes will be communicated in advance of updated billing.",
30 "fileDownload": "https://employ.niceremote.com/dashboard/file-preview?fileSlug=c4e0aa44-4b2b-4912-8d28-38e00422e044",
31 "inputType": "fieldset"
32 },
33 "properties": {
34 "employee_assistance_program": {
35 "enum": [
36 "no",
37 "Assistance Programs (Lifeworks - Employee Assistance)"
38 ],
39 "presentation": {
40 "inputType": "select",
41 "options": [
42 {
43 "label": "I don't want to offer this benefit.",
44 "value": "no"
45 },
46 {
47 "label": "Assistance Programs (Lifeworks - Employee Assistance)",
48 "value": "Assistance Programs (Lifeworks - Employee Assistance)"
49 }
50 ]
51 },
52 "title": "Employee Assistance Program",
53 "type": "string"
54 },
55 "health": {
56 "enum": [
57 "no",
58 "Basic - Employee Only (Canada Life - Basic Health Employee Only; Canada Life - Basic Dental Employee Only; Canada Life - Basic Vision; Canada Life - Basic Life; Canada Life - Basic AD&D)",
59 "Basic - Family (Canada Life - Basic Health Family; Canada Life - Basic Dental Family; Canada Life - Basic Vision; Canada Life - Basic Life; Canada Life - Basic AD&D)",
60 "Standard - Employee Only (Canada Life - Standard Health Employee Only; Canada Life - Standard Dental Employee Only; Canada Life - Standard Vision; Canada Life - Standard Life; Canada Life - Standard AD&D; Canada Life - Standard Short Term Disability; Canada Life - Standard Long Term Disability)",
61 "Standard - Family (Canada Life - Standard Health Family; Canada Life - Standard Dental Family; Canada Life - Standard Vision; Canada Life - Standard Life; Canada Life - Standard AD&D; Canada Life - Standard Short Term Disability; Canada Life - Standard Long Term Disability; Canada Life - Standard Dependent Life)",
62 "Plus - Employee Only (Canada Life - Plus Health Employee Only; Canada Life - Plus Dental Employee Only; Canada Life - Plus Vision; Canada Life - Plus Life; Canada Life - Plus AD&D; Canada Life - Plus Short Term Disability; Canada Life - Plus Long Term Disability)",
63 "Plus - Family (Canada Life - Plus Health Family; Canada Life - Plus Dental Family; Canada Life - Plus Vision; Canada Life - Plus Life; Canada Life - Plus AD&D; Canada Life - Plus Short Term Disability; Canada Life - Plus Long Term Disability; Canada Life - Plus Dependent Life)",
64 "Premium - Employee Only (Canada Life - Premium Health Employee Only; Canada Life - Premium Dental Employee Only; Canada Life - Premium Vision; Canada Life - Premium Life; Canada Life - Premium AD&D; Canada Life - Premium Short Term Disability; Canada Life - Premium Long Term Disability)",
65 "Premium - Family (Canada Life - Premium Health Family; Canada Life - Premium Dental Family; Canada Life - Premium Vision; Canada Life - Premium Life; Canada Life - Premium AD&D; Canada Life - Premium Short Term Disability; Canada Life - Premium Long Term Disability; Canada Life - Premium Dependent Life)"
66 ],
67 "presentation": {
68 "inputType": "select",
69 "options": [
70 {
71 "label": "I don't want to offer this benefit.",
72 "value": "no"
73 },
74 {
75 "label": "Basic - Employee Only (Canada Life - Basic Health Employee Only; Canada Life - Basic Dental Employee Only; Canada Life - Basic Vision; Canada Life - Basic Life; Canada Life - Basic AD&D)",
76 "value": "Basic - Employee Only (Canada Life - Basic Health Employee Only; Canada Life - Basic Dental Employee Only; Canada Life - Basic Vision; Canada Life - Basic Life; Canada Life - Basic AD&D)"
77 },
78 {
79 "label": "Basic - Family (Canada Life - Basic Health Family; Canada Life - Basic Dental Family; Canada Life - Basic Vision; Canada Life - Basic Life; Canada Life - Basic AD&D)",
80 "value": "Basic - Family (Canada Life - Basic Health Family; Canada Life - Basic Dental Family; Canada Life - Basic Vision; Canada Life - Basic Life; Canada Life - Basic AD&D)"
81 },
82 {
83 "label": "Standard - Employee Only (Canada Life - Standard Health Employee Only; Canada Life - Standard Dental Employee Only; Canada Life - Standard Vision; Canada Life - Standard Life; Canada Life - Standard AD&D; Canada Life - Standard Short Term Disability; Canada Life - Standard Long Term Disability)",
84 "value": "Standard - Employee Only (Canada Life - Standard Health Employee Only; Canada Life - Standard Dental Employee Only; Canada Life - Standard Vision; Canada Life - Standard Life; Canada Life - Standard AD&D; Canada Life - Standard Short Term Disability; Canada Life - Standard Long Term Disability)"
85 },
86 {
87 "label": "Standard - Family (Canada Life - Standard Health Family; Canada Life - Standard Dental Family; Canada Life - Standard Vision; Canada Life - Standard Life; Canada Life - Standard AD&D; Canada Life - Standard Short Term Disability; Canada Life - Standard Long Term Disability; Canada Life - Standard Dependent Life)",
88 "value": "Standard - Family (Canada Life - Standard Health Family; Canada Life - Standard Dental Family; Canada Life - Standard Vision; Canada Life - Standard Life; Canada Life - Standard AD&D; Canada Life - Standard Short Term Disability; Canada Life - Standard Long Term Disability; Canada Life - Standard Dependent Life)"
89 },
90 {
91 "label": "Plus - Employee Only (Canada Life - Plus Health Employee Only; Canada Life - Plus Dental Employee Only; Canada Life - Plus Vision; Canada Life - Plus Life; Canada Life - Plus AD&D; Canada Life - Plus Short Term Disability; Canada Life - Plus Long Term Disability)",
92 "value": "Plus - Employee Only (Canada Life - Plus Health Employee Only; Canada Life - Plus Dental Employee Only; Canada Life - Plus Vision; Canada Life - Plus Life; Canada Life - Plus AD&D; Canada Life - Plus Short Term Disability; Canada Life - Plus Long Term Disability)"
93 },
94 {
95 "label": "Plus - Family (Canada Life - Plus Health Family; Canada Life - Plus Dental Family; Canada Life - Plus Vision; Canada Life - Plus Life; Canada Life - Plus AD&D; Canada Life - Plus Short Term Disability; Canada Life - Plus Long Term Disability; Canada Life - Plus Dependent Life)",
96 "value": "Plus - Family (Canada Life - Plus Health Family; Canada Life - Plus Dental Family; Canada Life - Plus Vision; Canada Life - Plus Life; Canada Life - Plus AD&D; Canada Life - Plus Short Term Disability; Canada Life - Plus Long Term Disability; Canada Life - Plus Dependent Life)"
97 },
98 {
99 "label": "Premium - Employee Only (Canada Life - Premium Health Employee Only; Canada Life - Premium Dental Employee Only; Canada Life - Premium Vision; Canada Life - Premium Life; Canada Life - Premium AD&D; Canada Life - Premium Short Term Disability; Canada Life - Premium Long Term Disability)",
100 "value": "Premium - Employee Only (Canada Life - Premium Health Employee Only; Canada Life - Premium Dental Employee Only; Canada Life - Premium Vision; Canada Life - Premium Life; Canada Life - Premium AD&D; Canada Life - Premium Short Term Disability; Canada Life - Premium Long Term Disability)"
101 },
102 {
103 "label": "Premium - Family (Canada Life - Premium Health Family; Canada Life - Premium Dental Family; Canada Life - Premium Vision; Canada Life - Premium Life; Canada Life - Premium AD&D; Canada Life - Premium Short Term Disability; Canada Life - Premium Long Term Disability; Canada Life - Premium Dependent Life)",
104 "value": "Premium - Family (Canada Life - Premium Health Family; Canada Life - Premium Dental Family; Canada Life - Premium Vision; Canada Life - Premium Life; Canada Life - Premium AD&D; Canada Life - Premium Short Term Disability; Canada Life - Premium Long Term Disability; Canada Life - Premium Dependent Life)"
105 }
106 ]
107 },
108 "title": "Health",
109 "type": "string"
110 },
111 "retirement": {
112 "enum": [
113 "no",
114 "Basic Retirement (Canada Life - Basic Retirement)",
115 "Standard Retirement (Canada Life - Standard Retirement)",
116 "Plus Retirement (Canada Life - Plus Retirement)",
117 "Premium Retirement (Canada Life - Premium Retirement)"
118 ],
119 "presentation": {
120 "inputType": "select",
121 "options": [
122 {
123 "label": "I don't want to offer this benefit.",
124 "value": "no"
125 },
126 {
127 "label": "Basic Retirement (Canada Life - Basic Retirement)",
128 "value": "Basic Retirement (Canada Life - Basic Retirement)"
129 },
130 {
131 "label": "Standard Retirement (Canada Life - Standard Retirement)",
132 "value": "Standard Retirement (Canada Life - Standard Retirement)"
133 },
134 {
135 "label": "Plus Retirement (Canada Life - Plus Retirement)",
136 "value": "Plus Retirement (Canada Life - Plus Retirement)"
137 },
138 {
139 "label": "Premium Retirement (Canada Life - Premium Retirement)",
140 "value": "Premium Retirement (Canada Life - Premium Retirement)"
141 }
142 ]
143 },
144 "title": "Retirement",
145 "type": "string"
146 }
147 },
148 "required": ["employee_assistance_program", "health", "retirement"],
149 "title": "Benefits",
150 "type": "object",
151 "x-jsf-order": ["employee_assistance_program", "health", "retirement"]
152 },
153 "bonus_amount": {
154 "deprecated": true,
155 "presentation": {
156 "currency": "---",
157 "deprecated": {
158 "description": "Deprecated in favor of 'Bonus Details'. Please, try to leave this field empty."
159 },
160 "inputType": "money"
161 },
162 "readOnly": true,
163 "title": "Bonus amount (deprecated)",
164 "type": ["integer", "null"],
165 "x-jsf-errorMessage": {
166 "type": "Please, use US standard currency format. Ex: 1024.12"
167 }
168 },
169 "bonus_details": {
170 "description": "Please detail if this is a one-time bonus, quarterly bonus, yearly bonus or other. Please also indicate when this should be paid.",
171 "maxLength": 1000,
172 "presentation": {
173 "inputType": "textarea"
174 },
175 "title": "Bonus details",
176 "type": ["string", "null"]
177 },
178 "commissions_details": {
179 "description": "Please detail amounts and how often the commission will be paid - monthly, quarterly or yearly basis.",
180 "maxLength": 1000,
181 "presentation": {
182 "inputType": "textarea"
183 },
184 "title": "Commission details",
185 "type": ["string", "null"]
186 },
187 "company_business_description": {
188 "description": "Enter a summary of what your company does for a business. (e.g. Information Technology, Financial Services, Telecommunications, etc...)",
189 "maxLength": 1000,
190 "presentation": {
191 "inputType": "textarea"
192 },
193 "title": "Company's business description",
194 "type": "string"
195 },
196 "contract_duration": {
197 "deprecated": true,
198 "description": "Indefinite or fixed-term contract. If the latter, please state duration and if there's possibility for renewal.",
199 "maxLength": 255,
200 "presentation": {
201 "deprecated": {
202 "description": "Deprecated field in favor of 'contract_duration_type'."
203 },
204 "inputType": "text"
205 },
206 "readOnly": true,
207 "title": "Contract duration (deprecated)",
208 "type": ["string", "null"]
209 },
210 "contract_duration_type": {
211 "oneOf": [
212 {
213 "const": "indefinite",
214 "title": "Indefinite"
215 },
216 {
217 "const": "fixed_term",
218 "title": "Fixed Term"
219 }
220 ],
221 "presentation": {
222 "inputType": "radio"
223 },
224 "title": "Contract duration",
225 "type": "string"
226 },
227 "contract_end_date": {
228 "format": "date",
229 "maxLength": 255,
230 "presentation": {
231 "inputType": "date"
232 },
233 "title": "Contract end date",
234 "type": "string"
235 },
236 "equity_compensation": {
237 "description": "This is for tracking purposes only. Employment agreements will not include equity compensation. To offer equity, you need to work with your own lawyers and accountants to set up a plan that covers your team members.",
238 "presentation": {
239 "inputType": "fieldset"
240 },
241 "properties": {
242 "equity_cliff": {
243 "description": "When the first portion of the stock option grant will vest.",
244 "maximum": 100,
245 "presentation": {
246 "inputType": "number"
247 },
248 "title": "Cliff (in months)",
249 "type": "number"
250 },
251 "equity_vesting_period": {
252 "description": "The number of years it will take for the employee to vest all their options.",
253 "maximum": 100,
254 "presentation": {
255 "inputType": "number"
256 },
257 "title": "Vesting period (in years)",
258 "type": "number"
259 },
260 "number_of_stock_options": {
261 "description": "Tell us the type of equity you're granting as well.",
262 "maxLength": 255,
263 "presentation": {
264 "inputType": "text"
265 },
266 "title": "Number of options, RSUs, or other equity granted",
267 "type": "string"
268 },
269 "offer_equity_compensation": {
270 "oneOf": [
271 {
272 "const": "yes",
273 "title": "Yes"
274 },
275 {
276 "const": "no",
277 "title": "No"
278 }
279 ],
280 "presentation": {
281 "inputType": "radio"
282 },
283 "title": "Offer equity compensation?",
284 "type": "string"
285 }
286 },
287 "required": ["offer_equity_compensation"],
288 "title": "Equity compensation",
289 "type": "object",
290 "x-jsf-order": [
291 "offer_equity_compensation",
292 "number_of_stock_options",
293 "equity_cliff",
294 "equity_vesting_period"
295 ],
296 "allOf": [
297 {
298 "else": {
299 "properties": {
300 "equity_cliff": false,
301 "equity_vesting_period": false,
302 "number_of_stock_options": false
303 }
304 },
305 "if": {
306 "properties": {
307 "offer_equity_compensation": {
308 "const": "yes"
309 }
310 },
311 "required": ["offer_equity_compensation"]
312 },
313 "then": {
314 "required": [
315 "equity_cliff",
316 "equity_vesting_period",
317 "number_of_stock_options"
318 ]
319 }
320 }
321 ]
322 },
323 "experience_level": {
324 "description": "Please select the experience level that aligns with this role based on the job description (not the employees overall experience).",
325 "oneOf": [
326 {
327 "const": "Level 2 - Entry Level - Employees who perform operational tasks with an average level of complexity. They perform their functions with limited autonomy",
328 "description": "Employees who perform operational tasks with an average level of complexity. They perform their functions with limited autonomy",
329 "title": "Level 2 - Entry Level"
330 },
331 {
332 "const": "Level 3 - Associate - Employees who perform independently tasks and/or with coordination and control functions",
333 "description": "Employees who perform independently tasks and/or with coordination and control functions",
334 "title": "Level 3 - Associate"
335 },
336 {
337 "const": "Level 4 - Mid-Senior level - Employees with high professional functions, executive management responsibilities, who supervise the production with an initiative and operational autonomy within the responsibilities delegated to them",
338 "description": "Employees with high professional functions, executive management responsibilities, who supervise the production with an initiative and operational autonomy within the responsibilities delegated to them",
339 "title": "Level 4 - Mid-Senior level"
340 },
341 {
342 "const": "Level 5 - Director - Directors perform functions of an ongoing nature that are of significant importance for the development and implementation of the company's objectives",
343 "description": "Directors perform functions of an ongoing nature that are of significant importance for the development and implementation of the company's objectives",
344 "title": "Level 5 - Director"
345 },
346 {
347 "const": "Level 6 - Executive - An Executive is responsible for running an organization. They create plans to help their organizations grow",
348 "description": "An Executive is responsible for running an organization. They create plans to help their organizations grow",
349 "title": "Level 6 - Executive"
350 }
351 ],
352 "presentation": {
353 "inputType": "radio"
354 },
355 "title": "Experience level"
356 },
357 "has_bonus": {
358 "description": "These can be allowances or performance-related bonuses.",
359 "oneOf": [
360 {
361 "const": "yes",
362 "title": "Yes"
363 },
364 {
365 "const": "no",
366 "title": "No"
367 }
368 ],
369 "presentation": {
370 "inputType": "radio"
371 },
372 "title": "Offer other bonuses?",
373 "type": "string"
374 },
375 "has_commissions": {
376 "description": "You can outline your policy and pay commission to the employee on the platform. However, commission will not appear in the employment agreement. Please send full policy details directly to the employee.",
377 "oneOf": [
378 {
379 "const": "yes",
380 "title": "Yes"
381 },
382 {
383 "const": "no",
384 "title": "No"
385 }
386 ],
387 "presentation": {
388 "inputType": "radio"
389 },
390 "title": "Offer commission?",
391 "type": "string"
392 },
393 "has_signing_bonus": {
394 "description": "This is a one-time payment the employee receives when they join your team.",
395 "oneOf": [
396 {
397 "const": "yes",
398 "title": "Yes"
399 },
400 {
401 "const": "no",
402 "title": "No"
403 }
404 ],
405 "presentation": {
406 "inputType": "radio"
407 },
408 "title": "Offer a signing bonus?",
409 "type": "string"
410 },
411 "non_compete_clause": {
412 "description": "Do you wish to include a non-compete clause in the employee's contract?",
413 "oneOf": [
414 {
415 "const": "yes",
416 "title": "Yes"
417 },
418 {
419 "const": "no",
420 "title": "No"
421 }
422 ],
423 "presentation": {
424 "inputType": "radio"
425 },
426 "title": "Non-Compete Clause",
427 "type": "string"
428 },
429 "non_compete_clause_halt_period_months": {
430 "description": "How many months should the non-compete clause last after termination of the Employment Contract?",
431 "presentation": {
432 "inputType": "number"
433 },
434 "title": "Non-competition period (months)",
435 "type": "number"
436 },
437 "non_solicitation_clause": {
438 "description": "Do you wish to include a non-solicitation clause in the employee's contract?",
439 "oneOf": [
440 {
441 "const": "yes",
442 "title": "Yes"
443 },
444 {
445 "const": "no",
446 "title": "No"
447 }
448 ],
449 "presentation": {
450 "inputType": "radio"
451 },
452 "title": "Non-Solicitation Clause",
453 "type": "string"
454 },
455 "non_solicitation_clause_halt_period_months": {
456 "description": "How many months should the non-solicitation clause last after termination of the Employment Contract?",
457 "presentation": {
458 "inputType": "number"
459 },
460 "title": "Non-solicitation period (months)",
461 "type": "number"
462 },
463 "part_time_salary_confirmation": {
464 "const": "acknowledged",
465 "description": "I confirm the annual gross salary has been adjusted for part-time hours. Remote will use this gross salary in the employment agreement for the part-time employee.",
466 "presentation": {
467 "inputType": "checkbox"
468 },
469 "title": "Confirm part-time salary"
470 },
471 "probation_length": {
472 "description": "Please enter a value between 0 and 6 months (legal maximum).",
473 "presentation": {
474 "inputType": "number"
475 },
476 "title": "Probation period (in months)",
477 "type": ["number", "null"]
478 },
479 "probation_length_days": {
480 "description": "Please enter a value between 0 and 90 days (legal maximum).",
481 "presentation": {
482 "inputType": "number"
483 },
484 "title": "Probation period (in days)",
485 "type": ["number", "null"]
486 },
487 "probation_length_weeks": {
488 "description": "Please enter a value between 0 and 13 weeks (legal maximum).",
489 "presentation": {
490 "inputType": "number"
491 },
492 "title": "Probation period (in weeks)",
493 "type": ["number", "null"]
494 },
495 "province_of_residency": {
496 "oneOf": [
497 {
498 "const": "AB",
499 "title": "Alberta (AB)"
500 },
501 {
502 "const": "BC",
503 "title": "British Columbia (BC)"
504 },
505 {
506 "const": "MB",
507 "title": "Manitoba (MB)"
508 },
509 {
510 "const": "NB",
511 "title": "New Brunswick (NB)"
512 },
513 {
514 "const": "NL",
515 "title": "Newfoundland and Labrador (NL)"
516 },
517 {
518 "const": "NS",
519 "title": "Nova Scotia (NS)"
520 },
521 {
522 "const": "ON",
523 "title": "Ontario (ON)"
524 },
525 {
526 "const": "PE",
527 "title": "Prince Edward Island (PE)"
528 },
529 {
530 "const": "QC",
531 "title": "Quebec (QC)"
532 },
533 {
534 "const": "SK",
535 "title": "Saskatchewan (SK)"
536 }
537 ],
538 "presentation": {
539 "inputType": "select"
540 },
541 "title": "Employee's Canadian Province of Residence",
542 "type": "string"
543 },
544 "role_description": {
545 "description": "Please add top three actions and top three responsibilities that fall under this role.",
546 "maxLength": 10000,
547 "minLength": 100,
548 "presentation": {
549 "inputType": "textarea"
550 },
551 "title": "Role description",
552 "type": "string"
553 },
554 "signing_bonus_amount": {
555 "presentation": {
556 "currency": "CAD",
557 "inputType": "money"
558 },
559 "title": "Gross signing bonus",
560 "type": ["integer", "null"],
561 "x-jsf-errorMessage": {
562 "type": "Please, use US standard currency format. Ex: 1024.12"
563 }
564 },
565 "supervisor_name": {
566 "description": "Name of the direct supervisor or line manager",
567 "maxLength": 255,
568 "presentation": {
569 "inputType": "text"
570 },
571 "title": "Supervisor name",
572 "type": "string"
573 },
574 "training_required_for_position": {
575 "description": "Is there any specific training required for the position? If yes, please let us know.",
576 "maxLength": 10000,
577 "presentation": {
578 "inputType": "textarea"
579 },
580 "title": "Training requirement",
581 "type": ["string", "null"]
582 },
583 "work_address_is_home_address": {
584 "description": "Do you want the employee's home address to be their work address?",
585 "oneOf": [
586 {
587 "const": "yes",
588 "title": "Yes"
589 },
590 {
591 "const": "no",
592 "title": "No"
593 }
594 ],
595 "presentation": {
596 "inputType": "radio"
597 },
598 "title": "Work Address",
599 "type": "string"
600 },
601 "work_hours_per_week": {
602 "description": "Please indicate the number of hours the employee will work per week.",
603 "presentation": {
604 "inputType": "number"
605 },
606 "title": "Work hours per week",
607 "type": "number"
608 },
609 "work_schedule": {
610 "oneOf": [
611 {
612 "const": "full_time",
613 "title": "Full-time"
614 },
615 {
616 "const": "part_time",
617 "title": "Part-time"
618 }
619 ],
620 "presentation": {
621 "inputType": "radio"
622 },
623 "title": "Type of employee",
624 "type": "string"
625 }
626 },
627 "required": [
628 "annual_gross_salary",
629 "available_pto",
630 "benefits",
631 "company_business_description",
632 "contract_duration_type",
633 "equity_compensation",
634 "experience_level",
635 "has_signing_bonus",
636 "has_bonus",
637 "has_commissions",
638 "non_solicitation_clause",
639 "province_of_residency",
640 "role_description",
641 "supervisor_name",
642 "work_address_is_home_address",
643 "work_schedule"
644 ],
645 "type": "object",
646 "allOf": [
647 {
648 "else": {
649 "properties": {
650 "contract_end_date": false
651 }
652 },
653 "if": {
654 "properties": {
655 "contract_duration_type": {
656 "const": "fixed_term"
657 }
658 },
659 "required": ["contract_duration_type"]
660 },
661 "then": {
662 "required": ["contract_end_date"]
663 }
664 },
665 {
666 "else": {
667 "properties": {
668 "bonus_amount": false,
669 "bonus_details": false
670 }
671 },
672 "if": {
673 "properties": {
674 "has_bonus": {
675 "const": "yes"
676 }
677 },
678 "required": ["has_bonus"]
679 },
680 "then": {
681 "required": ["bonus_details"]
682 }
683 },
684 {
685 "else": {
686 "properties": {
687 "commissions_details": false
688 }
689 },
690 "if": {
691 "properties": {
692 "has_commissions": {
693 "const": "yes"
694 }
695 },
696 "required": ["has_commissions"]
697 },
698 "then": {
699 "required": ["commissions_details"]
700 }
701 },
702 {
703 "else": {
704 "properties": {
705 "signing_bonus_amount": false
706 }
707 },
708 "if": {
709 "properties": {
710 "has_signing_bonus": {
711 "const": "yes"
712 }
713 },
714 "required": ["has_signing_bonus"]
715 },
716 "then": {
717 "required": ["signing_bonus_amount"]
718 }
719 },
720 {
721 "if": {
722 "properties": {
723 "work_schedule": {
724 "const": "part_time"
725 }
726 },
727 "required": ["work_schedule"]
728 },
729 "then": {
730 "properties": {
731 "available_pto": {
732 "description": "Please note that Statutory, Bank Holidays and Public Holidays in the employee's country of residence are excluded from the above."
733 }
734 }
735 }
736 },
737 {
738 "else": {
739 "properties": {
740 "work_hours_per_week": {
741 "maximum": 29,
742 "minimum": 1
743 }
744 }
745 },
746 "if": {
747 "properties": {
748 "work_schedule": {
749 "const": "full_time"
750 }
751 },
752 "required": ["work_schedule"]
753 },
754 "then": {
755 "properties": {
756 "work_hours_per_week": {
757 "maximum": 40,
758 "minimum": 30
759 }
760 }
761 }
762 },
763 {
764 "if": {
765 "properties": {
766 "work_schedule": {
767 "const": "full_time"
768 }
769 },
770 "required": ["work_schedule"]
771 },
772 "then": {
773 "properties": {
774 "annual_gross_salary": {
775 "minimum": 2138500,
776 "x-jsf-errorMessage": {
777 "minimum": "CA$21,385.00 is the national minimum wage. Certain states and localities mandate a minimum wage higher than federal law. Remote reserves the right to increase the employee’s wage to adhere to the applicable laws. In case you have any doubts, please reach out to Remote so we can help you set the correct salary."
778 }
779 }
780 }
781 }
782 },
783 {
784 "else": {
785 "properties": {
786 "part_time_salary_confirmation": false
787 }
788 },
789 "if": {
790 "properties": {
791 "work_schedule": {
792 "const": "part_time"
793 }
794 },
795 "required": ["work_schedule"]
796 },
797 "then": {
798 "required": ["part_time_salary_confirmation"]
799 }
800 },
801 {
802 "else": {
803 "properties": {
804 "available_pto": {
805 "minimum": 10
806 }
807 }
808 },
809 "if": {
810 "properties": {
811 "work_schedule": {
812 "const": "part_time"
813 }
814 },
815 "required": ["work_schedule"]
816 },
817 "then": {
818 "properties": {
819 "available_pto": {
820 "minimum": 0
821 }
822 }
823 }
824 },
825 {
826 "else": {
827 "properties": {
828 "non_compete_clause_halt_period_months": false
829 }
830 },
831 "if": {
832 "properties": {
833 "non_compete_clause": {
834 "const": "yes"
835 }
836 },
837 "required": ["non_compete_clause"]
838 },
839 "then": {
840 "properties": {
841 "non_compete_clause_halt_period_months": {
842 "minimum": 1
843 }
844 },
845 "required": ["non_compete_clause_halt_period_months"]
846 }
847 },
848 {
849 "else": {
850 "properties": {
851 "non_solicitation_clause_halt_period_months": false
852 }
853 },
854 "if": {
855 "properties": {
856 "non_solicitation_clause": {
857 "const": "yes"
858 }
859 },
860 "required": ["non_solicitation_clause"]
861 },
862 "then": {
863 "properties": {
864 "non_solicitation_clause_halt_period_months": {
865 "minimum": 1
866 }
867 },
868 "required": ["non_solicitation_clause_halt_period_months"]
869 }
870 },
871 {
872 "else": {
873 "required": ["non_compete_clause"]
874 },
875 "if": {
876 "properties": {
877 "province_of_residency": {
878 "const": "ON"
879 }
880 },
881 "required": ["province_of_residency"]
882 },
883 "then": {
884 "properties": {
885 "non_compete_clause": false
886 }
887 }
888 },
889 {
890 "if": {
891 "properties": {
892 "province_of_residency": {
893 "const": "AB"
894 }
895 },
896 "required": ["province_of_residency"]
897 },
898 "then": {
899 "anyOf": [
900 {
901 "required": ["probation_length_days"]
902 },
903 {
904 "required": ["probation_length"]
905 }
906 ],
907 "properties": {
908 "probation_length_days": {
909 "description": "Please enter a value between 0 and 90 days (legal maximum).",
910 "maximum": 90,
911 "minimum": 0
912 }
913 }
914 }
915 },
916 {
917 "if": {
918 "properties": {
919 "province_of_residency": {
920 "const": "BC"
921 }
922 },
923 "required": ["province_of_residency"]
924 },
925 "then": {
926 "properties": {
927 "probation_length": {
928 "description": "Please enter a value between 0 and 3 months (legal maximum).",
929 "maximum": 3,
930 "minimum": 0
931 }
932 },
933 "required": ["probation_length"]
934 }
935 },
936 {
937 "if": {
938 "properties": {
939 "province_of_residency": {
940 "const": "MB"
941 }
942 },
943 "required": ["province_of_residency"]
944 },
945 "then": {
946 "anyOf": [
947 {
948 "required": ["probation_length_days"]
949 },
950 {
951 "required": ["probation_length"]
952 }
953 ],
954 "properties": {
955 "probation_length_days": {
956 "description": "Please enter a value between 0 and 30 days (legal maximum).",
957 "maximum": 30,
958 "minimum": 0
959 }
960 }
961 }
962 },
963 {
964 "if": {
965 "properties": {
966 "province_of_residency": {
967 "const": "NB"
968 }
969 },
970 "required": ["province_of_residency"]
971 },
972 "then": {
973 "properties": {
974 "probation_length": {
975 "description": "Please enter a value between 0 and 6 months (legal maximum).",
976 "maximum": 6,
977 "minimum": 0
978 }
979 },
980 "required": ["probation_length"]
981 }
982 },
983 {
984 "if": {
985 "properties": {
986 "province_of_residency": {
987 "const": "NL"
988 }
989 },
990 "required": ["province_of_residency"]
991 },
992 "then": {
993 "properties": {
994 "probation_length": {
995 "description": "Please enter a value between 0 and 3 months (legal maximum).",
996 "maximum": 3,
997 "minimum": 0
998 }
999 },
1000 "required": ["probation_length"]
1001 }
1002 },
1003 {
1004 "if": {
1005 "properties": {
1006 "province_of_residency": {
1007 "const": "NS"
1008 }
1009 },
1010 "required": ["province_of_residency"]
1011 },
1012 "then": {
1013 "properties": {
1014 "probation_length": {
1015 "description": "Please enter a value between 0 and 3 months (legal maximum).",
1016 "maximum": 3,
1017 "minimum": 0
1018 }
1019 },
1020 "required": ["probation_length"]
1021 }
1022 },
1023 {
1024 "if": {
1025 "properties": {
1026 "province_of_residency": {
1027 "const": "ON"
1028 }
1029 },
1030 "required": ["province_of_residency"]
1031 },
1032 "then": {
1033 "properties": {
1034 "probation_length": {
1035 "description": "Please enter a value between 0 and 3 months (legal maximum).",
1036 "maximum": 3,
1037 "minimum": 0
1038 }
1039 },
1040 "required": ["probation_length"]
1041 }
1042 },
1043 {
1044 "if": {
1045 "properties": {
1046 "province_of_residency": {
1047 "const": "PE"
1048 }
1049 },
1050 "required": ["province_of_residency"]
1051 },
1052 "then": {
1053 "properties": {
1054 "probation_length": {
1055 "description": "Please enter a value between 0 and 6 months (legal maximum).",
1056 "maximum": 6,
1057 "minimum": 0
1058 }
1059 },
1060 "required": ["probation_length"]
1061 }
1062 },
1063 {
1064 "if": {
1065 "properties": {
1066 "province_of_residency": {
1067 "const": "QC"
1068 }
1069 },
1070 "required": ["province_of_residency"]
1071 },
1072 "then": {
1073 "properties": {
1074 "probation_length": {
1075 "description": "Please enter a value between 0 and 3 months (legal maximum).",
1076 "maximum": 3,
1077 "minimum": 0
1078 }
1079 },
1080 "required": ["probation_length"]
1081 }
1082 },
1083 {
1084 "if": {
1085 "properties": {
1086 "province_of_residency": {
1087 "const": "SK"
1088 }
1089 },
1090 "required": ["province_of_residency"]
1091 },
1092 "then": {
1093 "anyOf": [
1094 {
1095 "required": ["probation_length_weeks"]
1096 },
1097 {
1098 "required": ["probation_length"]
1099 }
1100 ],
1101 "properties": {
1102 "probation_length_weeks": {
1103 "description": "Please enter a value between 0 and 13 weeks (legal maximum).",
1104 "maximum": 13,
1105 "minimum": 0
1106 }
1107 }
1108 }
1109 }
1110 ],
1111 "anyOf": [
1112 {
1113 "required": ["probation_length_days"]
1114 },
1115 {
1116 "required": ["probation_length_weeks"]
1117 },
1118 {
1119 "required": ["probation_length"]
1120 }
1121 ],
1122 "x-jsf-order": [
1123 "company_business_description",
1124 "province_of_residency",
1125 "contract_duration",
1126 "contract_duration_type",
1127 "contract_end_date",
1128 "probation_length",
1129 "probation_length_days",
1130 "probation_length_weeks",
1131 "work_schedule",
1132 "work_hours_per_week",
1133 "annual_gross_salary",
1134 "part_time_salary_confirmation",
1135 "has_signing_bonus",
1136 "signing_bonus_amount",
1137 "has_bonus",
1138 "bonus_amount",
1139 "bonus_details",
1140 "has_commissions",
1141 "commissions_details",
1142 "equity_compensation",
1143 "available_pto",
1144 "role_description",
1145 "supervisor_name",
1146 "experience_level",
1147 "training_required_for_position",
1148 "work_address_is_home_address",
1149 "non_compete_clause",
1150 "non_compete_clause_halt_period_months",
1151 "non_solicitation_clause",
1152 "non_solicitation_clause_halt_period_months",
1153 "benefits"
1154 ]
1155 },
1156 "version": 7
1157 }
1158}

Since the response is quite long, let’s take a look at the equity_compensation property, which makes use of many of the JSON schema features. Here’s what it looks like:

(Click to expand) The equity_compensation property.
json
1"equity_compensation": {
2 "description": "This is for tracking purposes only. Employment agreements will not include equity compensation. To offer equity, you need to work with your own lawyers and accountants to set up a plan that covers your team members.",
3 "presentation": {
4 "inputType": "fieldset"
5 },
6 "properties": {
7 "equity_cliff": {
8 "description": "When the first portion of the stock option grant will vest.",
9 "maximum": 100,
10 "presentation": {
11 "inputType": "number"
12 },
13 "title": "Cliff (in months)",
14 "type": "number"
15 },
16 "equity_vesting_period": {
17 "description": "The number of years it will take for the employee to vest all their options.",
18 "maximum": 100,
19 "presentation": {
20 "inputType": "number"
21 },
22 "title": "Vesting period (in years)",
23 "type": "number"
24 },
25 "number_of_stock_options": {
26 "description": "Tell us the type of equity you're granting as well.",
27 "maxLength": 255,
28 "presentation": {
29 "inputType": "text"
30 },
31 "title": "Number of options, RSUs, or other equity granted",
32 "type": "string"
33 },
34 "offer_equity_compensation": {
35 "oneOf": [
36 {
37 "const": "yes",
38 "title": "Yes"
39 },
40 {
41 "const": "no",
42 "title": "No"
43 }
44 ],
45 "presentation": {
46 "inputType": "radio"
47 },
48 "title": "Offer equity compensation?",
49 "type": "string"
50 }
51 },
52 "required": ["offer_equity_compensation"],
53 "title": "Equity compensation",
54 "type": "object",
55 "x-jsf-order": [
56 "offer_equity_compensation",
57 "number_of_stock_options",
58 "equity_cliff",
59 "equity_vesting_period"
60 ],
61 "allOf": [
62 {
63 "if": {
64 "properties": {
65 "offer_equity_compensation": {
66 "const": "yes"
67 }
68 },
69 "required": ["offer_equity_compensation"]
70 },
71 "then": {
72 "required": ["equity_cliff", "equity_vesting_period", "number_of_stock_options"]
73 },
74 "else": {
75 "properties": {
76 "equity_cliff": false,
77 "equity_vesting_period": false,
78 "number_of_stock_options": false
79 }
80 }
81 }
82 ]
83}

Title & Description

Let’s break that down. First, we have the title and description:

json
1{
2 "title": "Equity compensation",
3 "description": "This is for tracking purposes only. Employment agreements will not include equity compensation. To offer equity, you need to work with your own lawyers and accountants to set up a plan that covers your team members.",
4}

The title and description can be used if you’re displaying this field on a UI to collect data from employees. They’re informational and don’t have any other purpose.

Type and properties

The type is object, which indicates that this property has other properties stored under it. You can think of it as a “fieldset”, and it can be recursive.

json
1{
2 "type": "object",
3 "properties": {
4 "equity_cliff": { ... },
5 "equity_vesting_period": { ... },
6 "number_of_stock_options": { ... },
7 "offer_equity_compensation": { ... }
8 }
9}

This is telling us that equity_compensation accepts equity_cliff, equity_vesting_period, number_of_stock_options, and offer_equity_compensation as properties.

Now let’s look at each one of the fields. They also have title, description, and their own type. The offer_equity_compensation is slightly different, as it has oneOf, which means it can only take only one of the values defined inside it.

javascript
1"offer_equity_compensation": {
2 "oneOf": [
3 { "const": "yes", "title": "Yes" },
4 { "const": "no", "title": "No" }
5 ],
6 "title": "Offer equity compensation?"
7}

Validations

The required property specifies which fields must be sent. In this case, only the offer_equity_compensation needs to have a value by default.

json
1{
2 "required": ["offer_equity_compensation"]
3}

Be aware of all the possible validation keywords (e.g. maximum ) that might exist in each field. These are requirements, so fields that don’t meet their given requirements will throw validation errors when submitted via the Remote API.

Conditionals

Next, let’s look at the conditionals defined for this offer_equity_compensation:

json
1{
2 "allOf": [
3 {
4 "if": {
5 "properties": {
6 "offer_equity_compensation": {
7 "const": "yes"
8 }
9 },
10 "required": ["offer_equity_compensation"]
11 },
12 "then": {
13 "required": ["equity_cliff", "equity_vesting_period", "number_of_stock_options"]
14 },
15 "else": {
16 "properties": {
17 "equity_cliff": false,
18 "equity_vesting_period": false,
19 "number_of_stock_options": false
20 }
21 },
22 }
23 ],
24}

The allOf keyword accepts multiple validation rules. ALL must be satisfied. This one only has one conditional, using the if/then/else mechanism. It reads like this:

ℹ️
Due to the ordering priority given by our JSON schema engine, the if, then, and else properties are not always be in the order shown here. We’ve ordered them here for your convenience.

Watch out for additional conditionals

Usually, you’ll find a lot of validation rules in a JSON Schema. At Remote we use the allOf, oneOf, and anyOf keywords to compose multiple validations.

For more insights about validations, check .

Custom Keywords and x-jsf-* prefix

JSON Schema's purpose is to annotate and validate the structure of a JSON document, however, currently, it doesn't have any official spec when it comes to UI representation.

That's why you’ll find JSON Schemas with a few extra custom keywords, that are parsed by json-schema-form. We are currently renaming all the custom keywords to have the recommended x- prefix, plus a scoped prefix jsf-.

List of custom keywords:

  • x-jsf-presentation - used at each schema property (field)
    • Previously called presentation (migration in progress)
  • x-jsf-errorMessage - used at each schema property (field)
    • Previously called errorMessage
  • x-jsf-order - used at each schema root and property of type object (fieldset).
    • Replaced presentation.position (migration in progress)

Custom Keywords and x-jsf- prefix

JSON Schema's purpose is to annotate and validate the structure of a JSON document, however, currently, it doesn't have any official spec when it comes to UI representation.

That's why you’ll find a few extra custom keywords, such as x-jsf-presentation. These keywords are not part of the JSON schema specification, that’s why the use the recommended x- prefix.

We use these keywords to hint UI Libraries how to represent a given field. For example, the offer_equity_compensation has the following:

json
1"offer_equity_compensation": {
2 "x-jsf-presentation": {
3 "inputType": "radio"
4 },
5 "oneOf": [
6 { "const": "yes", "title": "Yes" },
7 { "const": "no", "title": "No" }
8 ],
9 "title": "Offer equity compensation?",
10}

This means we recommend representing this field as a radio. Note you’re not required to follow these visual suggestions. For example, you could choose to use a single-select dropdown instead of a radio group.

ℹ️
The x-jsf-* keywords are not part of the JSON schema specification. It is part of json-schema-form JavaScript library to improve the UX/UI of the forms generated by JSON schemas. You can read its specs in the Custom Keywords docs.

Creating a UI form to collect the data

To generate UI Forms based on JSON Schemas, we’ll use json-schema-form, a JavaScript library that we, Remote, created and use ourselves internally in the Remote Platform.

Why json-schema-form library?

JSON Schemas are great for API validation when it comes to creating employments. However, they are difficult to be used by the Frontend to build the respective UI Forms. On top of that, these JSON schemas are subject to change over time, so you need to ensure that UI forms generated from them are able to dynamically adapt to these changes automatically.

To that end, we created the json-schema-form library to make it easier for developers to automatically build UI forms based on the JSON schemas they receive from the API.

ℹ️
The json-schema-form is headless and does not require a any frameworks such as React, Vue, etc. It transforms JSON schemas received from the Remote API into JavaScript to be consumed by your UI libraries and generate the UI Forms.
🎉
json-schema-form is open-sourced! Check its Github repo https://github.com/remoteoss/json-schema-form/ and its documentation website for installation, demos, guides, and interactive playgrounds. If you were using the hardcoded version, please follow the migration guide:

How it works (Demo)

In this example, we will investigate how the library works with JSON schemas using a codesandbox demo. The demo uses React, but there’s no requirement for you to do so.

The demo already comes with a sample schema, located in the schema.json file. Its contents are a fragment of what you receive as a response from the show form schema endpoint.

On the right side, you will see a form rendered dynamically from the contents of the schema.json, using the JSF (json-schema-form library). This library parses the complex rules and conditionals defined in the JSON schema and then outputs a standardized JS object format that can be used to build forms in any way you choose.

Open the App.js file. You’ll see that we start by using createHeadlessForm to parse a JSON Schema into the headless form:

javascript
1// In your integration, you will populate the schema using the form endpoint
2import schema from "./schema.json";
3
4import { createHeadlessForm } from '@remoteoss/json-schema-form';
5
6const { fields, handleValidation } = createHeadlessForm(schema);

The library essentially goes through every property in the JSON schema, matches the respective validations and conditionals, and outputs the expected fields to build the Form.

Then, we integrate fields in our own form UI component. The validation is built on top of Yup, so you can use the handleValidation function to automatically handle validation based on schema constraints. Let’s see this in action:

javascript
1// For example, in React with the Formik library:
2function myForm() {
3 const { fields, handleValidation } = createHeadlessForm({ jsonSchema });
4
5 // ...
6
7 function handleValidate(formValues) {
8 const { formErrors } = handleValidation(formValues);
9 console.log({ formValues, formErrors });
10
11 return formErrors; // used by Formik interally
12 }
13
14 function handleOnSubmit(values) {
15 alert(JSON.stringify(values, null, 3));
16 console.log("Submitted!", values);
17 }
18
19 return (
20 <div>
21 <Formik
22 initialValues={initialValues}
23 validate={handleValidation}
24 onSubmit={handleOnSubmit}
25 >
26 {() => (
27 <Formik>
28 {fields.map((field) => {
29 if (field.isVisible === false) {
30 return null;
31 }
32
33 const FieldComponent = fieldsMapConfig[field.inputType];
34 return FieldComponent ? (
35 <FieldComponent key={field.name} {...field} />
36 ) : (
37 <Error>Field type {field.type} not supported</Error>
38 );
39 })}
40
41 <button type="submit">Submit</button>
42 </Formik>
43 )}
44 </Formik>
45 </div>
46 )
47}
ℹ️
In the Codesandbox, we use a library called Formik to render our form, but this is not required. You can even use json-schema-form in the backend on a Node server, as it’s framework-agnostic!

The validate prop callback is called by Formik whenever the values in our form fields change. Use it to call handleValidation, which dynamically mutates the fields and returns any errors to be passed back to Formik.

Fields

If you open the “Console” in the Codesandbox “Browser”, you can see the fields logged.

Here’s what the Fields format looks like for the equity_compensation property we were looking at earlier (click to expand)
json
1{
2 "inputType": "fieldset",
3 "name": "equity_compensation",
4 "label": "Equity compensation",
5 "fields": [
6 {
7 "inputType": "radio",
8 "name": "offer_equity_compensation",
9 "label": "Are you offering equity compensation?",
10 "options": [
11 { "label": "Yes", "value": "yes" },
12 { "label": "No", "value": "no" }
13 ],
14 "required": true,
15 "inputType": "radio",
16 "isVisible": true
17 },
18 {
19 "name": "number_of_stock_options",
20 "label": "Number of options, RSUs, or other equity granted",
21 "description": "Tell us the type of equity you're granting as well.",
22 "required": false,
23 "inputType": "text",
24 "maxLength": 255,
25 "isVisible": false,
26 "position": 1
27 },
28 {
29 "name": "equity_cliff",
30 "label": "Cliff (in months)",
31 "required": false,
32 "inputType": "number",
33 "description": "When the first portion of the stock option grant will vest.",
34 "maximum": 100,
35 "isVisible": false,
36 "position": 2
37 },
38 {
39 "name": "equity_vesting_period",
40 "label": "Vesting period (in years)",
41 "required": false,
42 "inputType": "number",
43 "description": "The number of years it will take for the employee to vest all their options.",
44 "maximum": 100,
45 "schema": {},
46 "isVisible": false,
47 "position": 3
48 }
49 ],
50 "inputType": "fieldset",
51 "required": true,
52 "description": "This is for tracking purposes only. Employment agreements will not include equity compensation. To offer equity, you need to work with your own lawyers and accountants to set up a plan that covers your team members.",
53 "isVisible": true
54}
ℹ️
You can read more about each Field format in the JSF docs.

Conditional validations

Let’s look at another fields: “Number of paid time off days” and “Work schedule” fields.

If you type “0” in the time-off field, you’ll see the error Must be greater or equal to 10:

But now, if you change the “Work schedule” to “Part-time”, you’ll see that the error goes away:

Let’s compare the field details that the library generates for us. For the “Number of paid time off days”, the before (left) and after (right) look like this:

json
1{
2 "type": "number",
3 "name": "available_pto",
4 "label": "Number of paid time off days",
5 "required": true,
6 "inputType": "number",
7 "jsonType": "integer",
8 "description": "We recommend at least 20 days.",
9 "isVisible": true,
10 "position": 13,
11 "minimum": 10
12}
json
1{
2 "type": "number",
3 "name": "available_pto",
4 "label": "Number of paid time off days",
5 "required": true,
6 "inputType": "number",
7 "jsonType": "integer",
8 "description": "Please note that Statutory, Bank Holidays and Public Holidays in the employee's country of residence are excluded from the above.",
9 "isVisible": true,
10 "position": 13,
11 "minimum": 0
12}

Notice that the description and minimum changed.

This mutation happened because of the JSON Schema conditional below, which was parsed by json-schema-form and re-evaluated through handleValidation().

json
1{
2 "if": {
3 "properties": {
4 "work_schedule": {
5 "const": "part_time"
6 }
7 },
8 "required": ["work_schedule"]
9 },
10 "then": {
11 "properties": {
12 "available_pto": {
13 "minimum": 0,
14 "description": "Please note that Statutory, Bank Holidays and Public Holidays in the employee's country of residence are excluded from the above."
15 }
16 }
17 },
18 "else": {
19 "properties": {
20 "available_pto": {
21 "minimum": 10
22 }
23 }
24 }
25}

Conditional fields

Let’s see another example with “Signing Bonus”. If you select “Yes” the field “Gross signing bonus” appears. This is what we call a “conditional field”.

This mutation happened because of the JSON Schema conditional below, which was parsed by json-schema-form and re-evaluated through handleValidation().

ℹ️
Every field is always declared in the root properties, even the conditional fields. When a given field is not accepted by default, we use the if/then/else to exclude it writing "signing_bonus_amount": false . Then json-schema-form parses this as a field to be invisible returning the field with isVisible: false.
json
1{
2 "if": {
3 "properties": {
4 "has_signing_bonus": {
5 "const": "yes"
6 }
7 },
8 "required": ["has_signing_bonus"]
9 },
10 "then": {
11 "required": ["signing_bonus_amount"]
12 },
13 "else": {
14 "properties": {
15 "signing_bonus_amount": false
16 }
17 }
18}

What’s Next?

There were some topics we didn’t cover in this guide, such as deprecated fields and money fields.

Dive into the json-schema-form docs demos to better understand how JSON schemas work with this library. Take special attention to the Important Concepts. Also, check its interactive Playground as it’s a great way to visualize how any JSON Schema could be represented in a UI Form.

Understanding JSON Schema validation errors

JSON Schema errors might be hard to interpret. We highly recommend you check jsonschemalint website to play around with JSON Schemas and better understand its validation.

Below it’s a JSON Schema based on the equity_compensation. Let’s see some possible errors:

JSON Schema example
json
1{
2 "properties": {
3 "equity_compensation": {
4 "description": "This is for tracking purposes only. Employment agreements will not include equity compensation. To offer equity, you need to work with your own lawyers and accountants to set up a plan that covers your team members.",
5 "presentation": {
6 "inputType": "fieldset"
7 },
8 "additionalProperties": false,
9 "properties": {
10 "equity_cliff": {
11 "description": "When the first portion of the stock option grant will vest.",
12 "maximum": 100,
13 "presentation": {
14 "inputType": "number"
15 },
16 "title": "Cliff (in months)",
17 "type": "number"
18 },
19 "equity_vesting_period": {
20 "description": "The number of years it will take for the employee to vest all their options.",
21 "maximum": 100,
22 "presentation": {
23 "inputType": "number"
24 },
25 "title": "Vesting period (in years)",
26 "type": "number"
27 },
28 "number_of_stock_options": {
29 "description": "Tell us the type of equity you're granting as well.",
30 "maxLength": 255,
31 "presentation": {
32 "inputType": "text"
33 },
34 "title": "Number of options, RSUs, or other equity granted",
35 "type": "string"
36 },
37 "offer_equity_compensation": {
38 "oneOf": [
39 {
40 "const": "yes",
41 "title": "Yes"
42 },
43 {
44 "const": "no",
45 "title": "No"
46 }
47 ],
48 "presentation": {
49 "inputType": "radio"
50 },
51 "title": "Offer equity compensation?",
52 "type": "string"
53 }
54 },
55 "required": [
56 "offer_equity_compensation"
57 ],
58 "title": "Equity compensation",
59 "type": "object",
60 "x-jsf-order": [
61 "offer_equity_compensation",
62 "number_of_stock_options",
63 "equity_cliff",
64 "equity_vesting_period"
65 ],
66 "allOf": [
67 {
68 "if": {
69 "properties": {
70 "offer_equity_compensation": {
71 "const": "yes"
72 }
73 },
74 "required": [
75 "offer_equity_compensation"
76 ]
77 },
78 "then": {
79 "required": [
80 "equity_cliff",
81 "equity_vesting_period",
82 "number_of_stock_options"
83 ]
84 },
85 "else": {
86 "properties": {
87 "equity_cliff": false,
88 "equity_vesting_period": false,
89 "number_of_stock_options": false
90 }
91 }
92 }
93 ]
94 }
95 },
96 "required": [
97 "equity_compensation"
98 ]
99}
JsonSchemaLint website: On the left panel you write a JSON Schema. On the right, you write the JSON data. In the bottom panel, you’ll see the validation errors.
JsonSchemaLint website: On the left panel you write a JSON Schema. On the right, you write the JSON data. In the bottom panel, you’ll see the validation errors.

json
1// Data:
2{
3 "equity_compensation": {}
4}
5
6// Error: "equity_compensation: Should have required property "offer_equity_compensation"

Explanation: It's required because the JSON Schema has inside the equity_compensation, the required: ["offer_equity_compensation"] .

json
1{
2 "equity_compensation": {
3 "offer_equity_compensation": "no"
4 }
5}
6
7// Error: n/a

Explanation: The data is valid, so there are no errors.

json
1{
2 "equity_compensation": {
3 "offer_equity_compensation": "foo"
4 }
5}
6
7// Error: "offer_equity_compensation: should match exactly one schema in oneOf"

Explanation: In the JSON Schema, the offer_equity_compensation has a oneOf with the explicit expected options (”no” or “yes”)

json
1{
2 "equity_compensation": {
3 "offer_equity_compensation": "yes"
4 }
5}
6
7// Errors:
8// "equity_compensation: should have required property 'equity_cliff'"
9// "equity_compensation: should have required property 'equity_vesting_period'"
10// "equity_compensation: should have required property 'number_of_stock_options'"

Explanation: As declared in the conditional allOf[0], if offer_equity_compensation is yes, then the remaining three fields are also required.

json
1{
2 "equity_compensation": {
3 "offer_equity_compensation": "yes",
4 "equity_cliff": 12,
5 "equity_vesting_period": 4,
6 "number_of_stock_options": "500 per year"
7 }
8}
9
10// Errors: N/A

Explanation: The data is valid, so there are no errors.

json
1{
2 "equity_compensation": {
3 "offer_equity_compensation": "no",
4 "equity_cliff": null
5 }
6}
7
8// Error: "equity_cliff: boolean schema is false"
9// Other validators: "equity_cliff: is invalid"

Explanation: When a JSON Schema declares a property as false, (in this case inside the else conditional) it means it’s not allowed, even as null. In this case, as offer_equity_compensation is "no", the equity_cliff is disallowed.

json
1{
2 "equity_compensation": {
3 "offer_equity_compensation": "no",
4 "something": "else"
5 }
6}
7
8// Error: "equity_compensation: should NOT have additional properties - something"

Explanation: This error happens because of "additionalProperties": “true” in the JSON Schema. This explicitly disallows properties not declared in the json schema.

Feedback & Questions

If you have any questions or feedback regarding the @remoteoss/json-schema-form library, you can reach us at api-support@remote.com, through Slack, or open an issue in the Github Repo.