How JSON Schemas Work
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:
- Are familiar with how to create an employment. If you’re not familiar with how to create an employment, you can follow this guide to create your first company and first employment. You will need an employment to follow the examples in this documentation.
- Have received company consent to act on their behalf. You will need a company-scoped access token to create and manage employments.
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
contract_details
schemaLet’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:
https://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:
$ curl --location \
--request GET 'https://gateway.remote-sandbox.com/v1/countries/CAN/contract_details' \
--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](https://gateway.remote.com/v1/docs/openapi.html#tag/Countries/operation/get_show_form_country) to obtain the latest schemas, as they’re dynamic and will change over time.**
contract_details
schema- (Click to expand) The entire response payload.
{ "data": { "schema": { "additionalProperties": false, "properties": { "annual_gross_salary": { "presentation": { "currency": "CAD", "inputType": "money" }, "title": "Annual gross salary", "type": "integer", "x-jsf-errorMessage": { "type": "Please, use US standard currency format. Ex: 1024.12" } }, "available_pto": { "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.", "presentation": { "inputType": "number" }, "title": "Number of paid time off days", "type": "number" }, "benefits": { "additionalProperties": false, "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.", "presentation": { "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.", "fileDownload": "https://employ.niceremote.com/dashboard/file-preview?fileSlug=c4e0aa44-4b2b-4912-8d28-38e00422e044", "inputType": "fieldset" }, "properties": { "employee_assistance_program": { "enum": [ "no", "Assistance Programs (Lifeworks - Employee Assistance)" ], "presentation": { "inputType": "select", "options": [ { "label": "I don't want to offer this benefit.", "value": "no" }, { "label": "Assistance Programs (Lifeworks - Employee Assistance)", "value": "Assistance Programs (Lifeworks - Employee Assistance)" } ] }, "title": "Employee Assistance Program", "type": "string" }, "health": { "enum": [ "no", "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)", "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)", "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)", "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)", "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)", "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)", "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)", "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)" ], "presentation": { "inputType": "select", "options": [ { "label": "I don't want to offer this benefit.", "value": "no" }, { "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)", "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)" }, { "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)", "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)" }, { "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)", "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)" }, { "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)", "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)" }, { "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)", "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)" }, { "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)", "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)" }, { "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)", "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)" }, { "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)", "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)" } ] }, "title": "Health", "type": "string" }, "retirement": { "enum": [ "no", "Basic Retirement (Canada Life - Basic Retirement)", "Standard Retirement (Canada Life - Standard Retirement)", "Plus Retirement (Canada Life - Plus Retirement)", "Premium Retirement (Canada Life - Premium Retirement)" ], "presentation": { "inputType": "select", "options": [ { "label": "I don't want to offer this benefit.", "value": "no" }, { "label": "Basic Retirement (Canada Life - Basic Retirement)", "value": "Basic Retirement (Canada Life - Basic Retirement)" }, { "label": "Standard Retirement (Canada Life - Standard Retirement)", "value": "Standard Retirement (Canada Life - Standard Retirement)" }, { "label": "Plus Retirement (Canada Life - Plus Retirement)", "value": "Plus Retirement (Canada Life - Plus Retirement)" }, { "label": "Premium Retirement (Canada Life - Premium Retirement)", "value": "Premium Retirement (Canada Life - Premium Retirement)" } ] }, "title": "Retirement", "type": "string" } }, "required": ["employee_assistance_program", "health", "retirement"], "title": "Benefits", "type": "object", "x-jsf-order": ["employee_assistance_program", "health", "retirement"] }, "bonus_amount": { "deprecated": true, "presentation": { "currency": "---", "deprecated": { "description": "Deprecated in favor of 'Bonus Details'. Please, try to leave this field empty." }, "inputType": "money" }, "readOnly": true, "title": "Bonus amount (deprecated)", "type": ["integer", "null"], "x-jsf-errorMessage": { "type": "Please, use US standard currency format. Ex: 1024.12" } }, "bonus_details": { "description": "Please detail if this is a one-time bonus, quarterly bonus, yearly bonus or other. Please also indicate when this should be paid.", "maxLength": 1000, "presentation": { "inputType": "textarea" }, "title": "Bonus details", "type": ["string", "null"] }, "commissions_details": { "description": "Please detail amounts and how often the commission will be paid - monthly, quarterly or yearly basis.", "maxLength": 1000, "presentation": { "inputType": "textarea" }, "title": "Commission details", "type": ["string", "null"] }, "company_business_description": { "description": "Enter a summary of what your company does for a business. (e.g. Information Technology, Financial Services, Telecommunications, etc...)", "maxLength": 1000, "presentation": { "inputType": "textarea" }, "title": "Company's business description", "type": "string" }, "contract_duration": { "deprecated": true, "description": "Indefinite or fixed-term contract. If the latter, please state duration and if there's possibility for renewal.", "maxLength": 255, "presentation": { "deprecated": { "description": "Deprecated field in favor of 'contract_duration_type'." }, "inputType": "text" }, "readOnly": true, "title": "Contract duration (deprecated)", "type": ["string", "null"] }, "contract_duration_type": { "oneOf": [ { "const": "indefinite", "title": "Indefinite" }, { "const": "fixed_term", "title": "Fixed Term" } ], "presentation": { "inputType": "radio" }, "title": "Contract duration", "type": "string" }, "contract_end_date": { "format": "date", "maxLength": 255, "presentation": { "inputType": "date" }, "title": "Contract end date", "type": "string" }, "equity_compensation": { "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.", "presentation": { "inputType": "fieldset" }, "properties": { "equity_cliff": { "description": "When the first portion of the stock option grant will vest.", "maximum": 100, "presentation": { "inputType": "number" }, "title": "Cliff (in months)", "type": "number" }, "equity_vesting_period": { "description": "The number of years it will take for the employee to vest all their options.", "maximum": 100, "presentation": { "inputType": "number" }, "title": "Vesting period (in years)", "type": "number" }, "number_of_stock_options": { "description": "Tell us the type of equity you're granting as well.", "maxLength": 255, "presentation": { "inputType": "text" }, "title": "Number of options, RSUs, or other equity granted", "type": "string" }, "offer_equity_compensation": { "oneOf": [ { "const": "yes", "title": "Yes" }, { "const": "no", "title": "No" } ], "presentation": { "inputType": "radio" }, "title": "Offer equity compensation?", "type": "string" } }, "required": ["offer_equity_compensation"], "title": "Equity compensation", "type": "object", "x-jsf-order": [ "offer_equity_compensation", "number_of_stock_options", "equity_cliff", "equity_vesting_period" ], "allOf": [ { "else": { "properties": { "equity_cliff": false, "equity_vesting_period": false, "number_of_stock_options": false } }, "if": { "properties": { "offer_equity_compensation": { "const": "yes" } }, "required": ["offer_equity_compensation"] }, "then": { "required": [ "equity_cliff", "equity_vesting_period", "number_of_stock_options" ] } } ] }, "experience_level": { "description": "Please select the experience level that aligns with this role based on the job description (not the employees overall experience).", "oneOf": [ { "const": "Level 2 - Entry Level - Employees who perform operational tasks with an average level of complexity. They perform their functions with limited autonomy", "description": "Employees who perform operational tasks with an average level of complexity. They perform their functions with limited autonomy", "title": "Level 2 - Entry Level" }, { "const": "Level 3 - Associate - Employees who perform independently tasks and/or with coordination and control functions", "description": "Employees who perform independently tasks and/or with coordination and control functions", "title": "Level 3 - Associate" }, { "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", "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", "title": "Level 4 - Mid-Senior level" }, { "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", "description": "Directors perform functions of an ongoing nature that are of significant importance for the development and implementation of the company's objectives", "title": "Level 5 - Director" }, { "const": "Level 6 - Executive - An Executive is responsible for running an organization. They create plans to help their organizations grow", "description": "An Executive is responsible for running an organization. They create plans to help their organizations grow", "title": "Level 6 - Executive" } ], "presentation": { "inputType": "radio" }, "title": "Experience level" }, "has_bonus": { "description": "These can be allowances or performance-related bonuses.", "oneOf": [ { "const": "yes", "title": "Yes" }, { "const": "no", "title": "No" } ], "presentation": { "inputType": "radio" }, "title": "Offer other bonuses?", "type": "string" }, "has_commissions": { "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.", "oneOf": [ { "const": "yes", "title": "Yes" }, { "const": "no", "title": "No" } ], "presentation": { "inputType": "radio" }, "title": "Offer commission?", "type": "string" }, "has_signing_bonus": { "description": "This is a one-time payment the employee receives when they join your team.", "oneOf": [ { "const": "yes", "title": "Yes" }, { "const": "no", "title": "No" } ], "presentation": { "inputType": "radio" }, "title": "Offer a signing bonus?", "type": "string" }, "non_compete_clause": { "description": "Do you wish to include a non-compete clause in the employee's contract?", "oneOf": [ { "const": "yes", "title": "Yes" }, { "const": "no", "title": "No" } ], "presentation": { "inputType": "radio" }, "title": "Non-Compete Clause", "type": "string" }, "non_compete_clause_halt_period_months": { "description": "How many months should the non-compete clause last after termination of the Employment Contract?", "presentation": { "inputType": "number" }, "title": "Non-competition period (months)", "type": "number" }, "non_solicitation_clause": { "description": "Do you wish to include a non-solicitation clause in the employee's contract?", "oneOf": [ { "const": "yes", "title": "Yes" }, { "const": "no", "title": "No" } ], "presentation": { "inputType": "radio" }, "title": "Non-Solicitation Clause", "type": "string" }, "non_solicitation_clause_halt_period_months": { "description": "How many months should the non-solicitation clause last after termination of the Employment Contract?", "presentation": { "inputType": "number" }, "title": "Non-solicitation period (months)", "type": "number" }, "part_time_salary_confirmation": { "const": "acknowledged", "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.", "presentation": { "inputType": "checkbox" }, "title": "Confirm part-time salary" }, "probation_length": { "description": "Please enter a value between 0 and 6 months (legal maximum).", "presentation": { "inputType": "number" }, "title": "Probation period (in months)", "type": ["number", "null"] }, "probation_length_days": { "description": "Please enter a value between 0 and 90 days (legal maximum).", "presentation": { "inputType": "number" }, "title": "Probation period (in days)", "type": ["number", "null"] }, "probation_length_weeks": { "description": "Please enter a value between 0 and 13 weeks (legal maximum).", "presentation": { "inputType": "number" }, "title": "Probation period (in weeks)", "type": ["number", "null"] }, "province_of_residency": { "oneOf": [ { "const": "AB", "title": "Alberta (AB)" }, { "const": "BC", "title": "British Columbia (BC)" }, { "const": "MB", "title": "Manitoba (MB)" }, { "const": "NB", "title": "New Brunswick (NB)" }, { "const": "NL", "title": "Newfoundland and Labrador (NL)" }, { "const": "NS", "title": "Nova Scotia (NS)" }, { "const": "ON", "title": "Ontario (ON)" }, { "const": "PE", "title": "Prince Edward Island (PE)" }, { "const": "QC", "title": "Quebec (QC)" }, { "const": "SK", "title": "Saskatchewan (SK)" } ], "presentation": { "inputType": "select" }, "title": "Employee's Canadian Province of Residence", "type": "string" }, "role_description": { "description": "Please add top three actions and top three responsibilities that fall under this role.", "maxLength": 10000, "minLength": 100, "presentation": { "inputType": "textarea" }, "title": "Role description", "type": "string" }, "signing_bonus_amount": { "presentation": { "currency": "CAD", "inputType": "money" }, "title": "Gross signing bonus", "type": ["integer", "null"], "x-jsf-errorMessage": { "type": "Please, use US standard currency format. Ex: 1024.12" } }, "supervisor_name": { "description": "Name of the direct supervisor or line manager", "maxLength": 255, "presentation": { "inputType": "text" }, "title": "Supervisor name", "type": "string" }, "training_required_for_position": { "description": "Is there any specific training required for the position? If yes, please let us know.", "maxLength": 10000, "presentation": { "inputType": "textarea" }, "title": "Training requirement", "type": ["string", "null"] }, "work_address_is_home_address": { "description": "Do you want the employee's home address to be their work address?", "oneOf": [ { "const": "yes", "title": "Yes" }, { "const": "no", "title": "No" } ], "presentation": { "inputType": "radio" }, "title": "Work Address", "type": "string" }, "work_hours_per_week": { "description": "Please indicate the number of hours the employee will work per week.", "presentation": { "inputType": "number" }, "title": "Work hours per week", "type": "number" }, "work_schedule": { "oneOf": [ { "const": "full_time", "title": "Full-time" }, { "const": "part_time", "title": "Part-time" } ], "presentation": { "inputType": "radio" }, "title": "Type of employee", "type": "string" } }, "required": [ "annual_gross_salary", "available_pto", "benefits", "company_business_description", "contract_duration_type", "equity_compensation", "experience_level", "has_signing_bonus", "has_bonus", "has_commissions", "non_solicitation_clause", "province_of_residency", "role_description", "supervisor_name", "work_address_is_home_address", "work_schedule" ], "type": "object", "allOf": [ { "else": { "properties": { "contract_end_date": false } }, "if": { "properties": { "contract_duration_type": { "const": "fixed_term" } }, "required": ["contract_duration_type"] }, "then": { "required": ["contract_end_date"] } }, { "else": { "properties": { "bonus_amount": false, "bonus_details": false } }, "if": { "properties": { "has_bonus": { "const": "yes" } }, "required": ["has_bonus"] }, "then": { "required": ["bonus_details"] } }, { "else": { "properties": { "commissions_details": false } }, "if": { "properties": { "has_commissions": { "const": "yes" } }, "required": ["has_commissions"] }, "then": { "required": ["commissions_details"] } }, { "else": { "properties": { "signing_bonus_amount": false } }, "if": { "properties": { "has_signing_bonus": { "const": "yes" } }, "required": ["has_signing_bonus"] }, "then": { "required": ["signing_bonus_amount"] } }, { "if": { "properties": { "work_schedule": { "const": "part_time" } }, "required": ["work_schedule"] }, "then": { "properties": { "available_pto": { "description": "Please note that Statutory, Bank Holidays and Public Holidays in the employee's country of residence are excluded from the above." } } } }, { "else": { "properties": { "work_hours_per_week": { "maximum": 29, "minimum": 1 } } }, "if": { "properties": { "work_schedule": { "const": "full_time" } }, "required": ["work_schedule"] }, "then": { "properties": { "work_hours_per_week": { "maximum": 40, "minimum": 30 } } } }, { "if": { "properties": { "work_schedule": { "const": "full_time" } }, "required": ["work_schedule"] }, "then": { "properties": { "annual_gross_salary": { "minimum": 2138500, "x-jsf-errorMessage": { "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." } } } } }, { "else": { "properties": { "part_time_salary_confirmation": false } }, "if": { "properties": { "work_schedule": { "const": "part_time" } }, "required": ["work_schedule"] }, "then": { "required": ["part_time_salary_confirmation"] } }, { "else": { "properties": { "available_pto": { "minimum": 10 } } }, "if": { "properties": { "work_schedule": { "const": "part_time" } }, "required": ["work_schedule"] }, "then": { "properties": { "available_pto": { "minimum": 0 } } } }, { "else": { "properties": { "non_compete_clause_halt_period_months": false } }, "if": { "properties": { "non_compete_clause": { "const": "yes" } }, "required": ["non_compete_clause"] }, "then": { "properties": { "non_compete_clause_halt_period_months": { "minimum": 1 } }, "required": ["non_compete_clause_halt_period_months"] } }, { "else": { "properties": { "non_solicitation_clause_halt_period_months": false } }, "if": { "properties": { "non_solicitation_clause": { "const": "yes" } }, "required": ["non_solicitation_clause"] }, "then": { "properties": { "non_solicitation_clause_halt_period_months": { "minimum": 1 } }, "required": ["non_solicitation_clause_halt_period_months"] } }, { "else": { "required": ["non_compete_clause"] }, "if": { "properties": { "province_of_residency": { "const": "ON" } }, "required": ["province_of_residency"] }, "then": { "properties": { "non_compete_clause": false } } }, { "if": { "properties": { "province_of_residency": { "const": "AB" } }, "required": ["province_of_residency"] }, "then": { "anyOf": [ { "required": ["probation_length_days"] }, { "required": ["probation_length"] } ], "properties": { "probation_length_days": { "description": "Please enter a value between 0 and 90 days (legal maximum).", "maximum": 90, "minimum": 0 } } } }, { "if": { "properties": { "province_of_residency": { "const": "BC" } }, "required": ["province_of_residency"] }, "then": { "properties": { "probation_length": { "description": "Please enter a value between 0 and 3 months (legal maximum).", "maximum": 3, "minimum": 0 } }, "required": ["probation_length"] } }, { "if": { "properties": { "province_of_residency": { "const": "MB" } }, "required": ["province_of_residency"] }, "then": { "anyOf": [ { "required": ["probation_length_days"] }, { "required": ["probation_length"] } ], "properties": { "probation_length_days": { "description": "Please enter a value between 0 and 30 days (legal maximum).", "maximum": 30, "minimum": 0 } } } }, { "if": { "properties": { "province_of_residency": { "const": "NB" } }, "required": ["province_of_residency"] }, "then": { "properties": { "probation_length": { "description": "Please enter a value between 0 and 6 months (legal maximum).", "maximum": 6, "minimum": 0 } }, "required": ["probation_length"] } }, { "if": { "properties": { "province_of_residency": { "const": "NL" } }, "required": ["province_of_residency"] }, "then": { "properties": { "probation_length": { "description": "Please enter a value between 0 and 3 months (legal maximum).", "maximum": 3, "minimum": 0 } }, "required": ["probation_length"] } }, { "if": { "properties": { "province_of_residency": { "const": "NS" } }, "required": ["province_of_residency"] }, "then": { "properties": { "probation_length": { "description": "Please enter a value between 0 and 3 months (legal maximum).", "maximum": 3, "minimum": 0 } }, "required": ["probation_length"] } }, { "if": { "properties": { "province_of_residency": { "const": "ON" } }, "required": ["province_of_residency"] }, "then": { "properties": { "probation_length": { "description": "Please enter a value between 0 and 3 months (legal maximum).", "maximum": 3, "minimum": 0 } }, "required": ["probation_length"] } }, { "if": { "properties": { "province_of_residency": { "const": "PE" } }, "required": ["province_of_residency"] }, "then": { "properties": { "probation_length": { "description": "Please enter a value between 0 and 6 months (legal maximum).", "maximum": 6, "minimum": 0 } }, "required": ["probation_length"] } }, { "if": { "properties": { "province_of_residency": { "const": "QC" } }, "required": ["province_of_residency"] }, "then": { "properties": { "probation_length": { "description": "Please enter a value between 0 and 3 months (legal maximum).", "maximum": 3, "minimum": 0 } }, "required": ["probation_length"] } }, { "if": { "properties": { "province_of_residency": { "const": "SK" } }, "required": ["province_of_residency"] }, "then": { "anyOf": [ { "required": ["probation_length_weeks"] }, { "required": ["probation_length"] } ], "properties": { "probation_length_weeks": { "description": "Please enter a value between 0 and 13 weeks (legal maximum).", "maximum": 13, "minimum": 0 } } } } ], "anyOf": [ { "required": ["probation_length_days"] }, { "required": ["probation_length_weeks"] }, { "required": ["probation_length"] } ], "x-jsf-order": [ "company_business_description", "province_of_residency", "contract_duration", "contract_duration_type", "contract_end_date", "probation_length", "probation_length_days", "probation_length_weeks", "work_schedule", "work_hours_per_week", "annual_gross_salary", "part_time_salary_confirmation", "has_signing_bonus", "signing_bonus_amount", "has_bonus", "bonus_amount", "bonus_details", "has_commissions", "commissions_details", "equity_compensation", "available_pto", "role_description", "supervisor_name", "experience_level", "training_required_for_position", "work_address_is_home_address", "non_compete_clause", "non_compete_clause_halt_period_months", "non_solicitation_clause", "non_solicitation_clause_halt_period_months", "benefits" ] }, "version": 7 } }
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."equity_compensation": { "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.", "presentation": { "inputType": "fieldset" }, "properties": { "equity_cliff": { "description": "When the first portion of the stock option grant will vest.", "maximum": 100, "presentation": { "inputType": "number" }, "title": "Cliff (in months)", "type": "number" }, "equity_vesting_period": { "description": "The number of years it will take for the employee to vest all their options.", "maximum": 100, "presentation": { "inputType": "number" }, "title": "Vesting period (in years)", "type": "number" }, "number_of_stock_options": { "description": "Tell us the type of equity you're granting as well.", "maxLength": 255, "presentation": { "inputType": "text" }, "title": "Number of options, RSUs, or other equity granted", "type": "string" }, "offer_equity_compensation": { "oneOf": [ { "const": "yes", "title": "Yes" }, { "const": "no", "title": "No" } ], "presentation": { "inputType": "radio" }, "title": "Offer equity compensation?", "type": "string" } }, "required": ["offer_equity_compensation"], "title": "Equity compensation", "type": "object", "x-jsf-order": [ "offer_equity_compensation", "number_of_stock_options", "equity_cliff", "equity_vesting_period" ], "allOf": [ { "if": { "properties": { "offer_equity_compensation": { "const": "yes" } }, "required": ["offer_equity_compensation"] }, "then": { "required": ["equity_cliff", "equity_vesting_period", "number_of_stock_options"] }, "else": { "properties": { "equity_cliff": false, "equity_vesting_period": false, "number_of_stock_options": false } } } ] }
Title & Description
Let’s break that down. First, we have the title
and description
:
{
"title": "Equity compensation",
"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.",
}
The [title
and description
](https://json-schema.org/draft/2020-12/json-schema-validation.html#section-9.1) 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](https://json-schema.org/draft/2020-12/json-schema-validation.html#section-6.1.1)
is object
, which indicates that this property has other [properties](https://json-schema.org/draft/2020-12/json-schema-core.html#section-10.3.2.1)
stored under it. You can think of it as a “fieldset”, and it can be recursive.
{
"type": "object",
"properties": {
"equity_cliff": { ... },
"equity_vesting_period": { ... },
"number_of_stock_options": { ... },
"offer_equity_compensation": { ... }
}
}
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.
"offer_equity_compensation": {
"oneOf": [
{ "const": "yes", "title": "Yes" },
{ "const": "no", "title": "No" }
],
"title": "Offer equity compensation?"
}
Validations
The [required](https://json-schema.org/draft/2020-12/json-schema-validation.html#section-6.5.3)
property specifies which fields must be sent. In this case, only the offer_equity_compensation
needs to have a value by default.
{
"required": ["offer_equity_compensation"]
}
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
:
{
"allOf": [
{
"if": {
"properties": {
"offer_equity_compensation": {
"const": "yes"
}
},
"required": ["offer_equity_compensation"]
},
"then": {
"required": ["equity_cliff", "equity_vesting_period", "number_of_stock_options"]
},
"else": {
"properties": {
"equity_cliff": false,
"equity_vesting_period": false,
"number_of_stock_options": false
}
},
}
],
}
The [allOf
keyword](https://json-schema.org/understanding-json-schema/reference/combining.html?highlight=allof#allof) accepts multiple validation rules. ALL must be satisfied. This one only has one conditional, using the if/then/else mechanism. It reads like this:
If the offer_equity_compensation
property has the value yes
,
Then the equity_cliff
, equity_vesting_period
, and number_of_stock_options
are required too.
Otherwise, these three properties are not allowed to be sent.
ℹ️ 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](https://json-schema.org/understanding-json-schema/reference/combining.html?highlight=allof#allof) to compose multiple validations.
For more insights about validations, check Understanding JSON Schema validation errors.
Custom Keywords and x-jsf-*
prefix
x-jsf-*
prefixJSON 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)
- Previously called
x-jsf-errorMessage
- used at each schema property (field)- Previously called
errorMessage
- Previously called
x-jsf-order
- used at each schema root and property of type object (fieldset).- Replaced
presentation.position
(migration in progress)
- Replaced
Custom Keywords and x-jsf-
prefix
x-jsf-
prefixJSON 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:
"offer_equity_compensation": {
"x-jsf-presentation": {
"inputType": "radio"
},
"oneOf": [
{ "const": "yes", "title": "Yes" },
{ "const": "no", "title": "No" }
],
"title": "Offer equity compensation?",
}
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.
HelpCenter Keyword
Head over here.
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-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.
If you were using the hardcoded version, please follow the migration guide:
Migration codesandbox → 0.1.0-beta
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.
- 🕹 Codesandbox demo - Fork it to follow along more easily.
- 🎥 Video walkthrough - An alternative to the written docs below.
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:
// In your integration, you will populate the schema using the form endpoint
import schema from "./schema.json";
import { createHeadlessForm } from '@remoteoss/json-schema-form';
const { 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:
// For example, in React with the Formik library:
function myForm() {
const { fields, handleValidation } = createHeadlessForm({ jsonSchema });
// ...
function handleValidate(formValues) {
const { formErrors } = handleValidation(formValues);
console.log({ formValues, formErrors });
return formErrors; // used by Formik interally
}
function handleOnSubmit(values) {
alert(JSON.stringify(values, null, 3));
console.log("Submitted!", values);
}
return (
<div>
<Formik
initialValues={initialValues}
validate={handleValidation}
onSubmit={handleOnSubmit}
>
{() => (
<Formik>
{fields.map((field) => {
if (field.isVisible === false) {
return null;
}
const FieldComponent = fieldsMapConfig[field.inputType];
return FieldComponent ? (
<FieldComponent key={field.name} {...field} />
) : (
<Error>Field type {field.type} not supported</Error>
);
})}
<button type="submit">Submit</button>
</Formik>
)}
</Formik>
</div>
)
}
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){ "inputType": "fieldset", "name": "equity_compensation", "label": "Equity compensation", "fields": [ { "inputType": "radio", "name": "offer_equity_compensation", "label": "Are you offering equity compensation?", "options": [ { "label": "Yes", "value": "yes" }, { "label": "No", "value": "no" } ], "required": true, "inputType": "radio", "isVisible": true }, { "name": "number_of_stock_options", "label": "Number of options, RSUs, or other equity granted", "description": "Tell us the type of equity you're granting as well.", "required": false, "inputType": "text", "maxLength": 255, "isVisible": false, "position": 1 }, { "name": "equity_cliff", "label": "Cliff (in months)", "required": false, "inputType": "number", "description": "When the first portion of the stock option grant will vest.", "maximum": 100, "isVisible": false, "position": 2 }, { "name": "equity_vesting_period", "label": "Vesting period (in years)", "required": false, "inputType": "number", "description": "The number of years it will take for the employee to vest all their options.", "maximum": 100, "schema": {}, "isVisible": false, "position": 3 } ], "inputType": "fieldset", "required": true, "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.", "isVisible": true }
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:
{
"type": "number",
"name": "available_pto",
"label": "Number of paid time off days",
"required": true,
"inputType": "number",
"jsonType": "integer",
"description": "We recommend at least 20 days.",
"isVisible": true,
"position": 13,
"minimum": 10
}
{
"type": "number",
"name": "available_pto",
"label": "Number of paid time off days",
"required": true,
"inputType": "number",
"jsonType": "integer",
"description": "Please note that Statutory, Bank Holidays and Public Holidays in the employee's country of residence are excluded from the above.",
"isVisible": true,
"position": 13,
"minimum": 0
}
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()
.
{
"if": {
"properties": {
"work_schedule": {
"const": "part_time"
}
},
"required": ["work_schedule"]
},
"then": {
"properties": {
"available_pto": {
"minimum": 0,
"description": "Please note that Statutory, Bank Holidays and Public Holidays in the employee's country of residence are excluded from the above."
}
}
},
"else": {
"properties": {
"available_pto": {
"minimum": 10
}
}
}
}
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()
.
{
"if": {
"properties": {
"has_signing_bonus": {
"const": "yes"
}
},
"required": ["has_signing_bonus"]
},
"then": {
"required": ["signing_bonus_amount"]
},
"else": {
"properties": {
"signing_bonus_amount": false
}
}
}
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
{ "properties": { "equity_compensation": { "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.", "presentation": { "inputType": "fieldset" }, "additionalProperties": false, "properties": { "equity_cliff": { "description": "When the first portion of the stock option grant will vest.", "maximum": 100, "presentation": { "inputType": "number" }, "title": "Cliff (in months)", "type": "number" }, "equity_vesting_period": { "description": "The number of years it will take for the employee to vest all their options.", "maximum": 100, "presentation": { "inputType": "number" }, "title": "Vesting period (in years)", "type": "number" }, "number_of_stock_options": { "description": "Tell us the type of equity you're granting as well.", "maxLength": 255, "presentation": { "inputType": "text" }, "title": "Number of options, RSUs, or other equity granted", "type": "string" }, "offer_equity_compensation": { "oneOf": [ { "const": "yes", "title": "Yes" }, { "const": "no", "title": "No" } ], "presentation": { "inputType": "radio" }, "title": "Offer equity compensation?", "type": "string" } }, "required": [ "offer_equity_compensation" ], "title": "Equity compensation", "type": "object", "x-jsf-order": [ "offer_equity_compensation", "number_of_stock_options", "equity_cliff", "equity_vesting_period" ], "allOf": [ { "if": { "properties": { "offer_equity_compensation": { "const": "yes" } }, "required": [ "offer_equity_compensation" ] }, "then": { "required": [ "equity_cliff", "equity_vesting_period", "number_of_stock_options" ] }, "else": { "properties": { "equity_cliff": false, "equity_vesting_period": false, "number_of_stock_options": false } } } ] } }, "required": [ "equity_compensation" ] }
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.
// Data:
{
"equity_compensation": {}
}
// 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"]
.
{
"equity_compensation": {
"offer_equity_compensation": "no"
}
}
// Error: n/a
Explanation: The data is valid, so there are no errors.
{
"equity_compensation": {
"offer_equity_compensation": "foo"
}
}
// 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”)
{
"equity_compensation": {
"offer_equity_compensation": "yes"
}
}
// Errors:
// "equity_compensation: should have required property 'equity_cliff'"
// "equity_compensation: should have required property 'equity_vesting_period'"
// "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.
{
"equity_compensation": {
"offer_equity_compensation": "yes",
"equity_cliff": 12,
"equity_vesting_period": 4,
"number_of_stock_options": "500 per year"
}
}
// Errors: N/A
Explanation: The data is valid, so there are no errors.
{
"equity_compensation": {
"offer_equity_compensation": "no",
"equity_cliff": null
}
}
// Error: "equity_cliff: boolean schema is false"
// 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.
{
"equity_compensation": {
"offer_equity_compensation": "no",
"something": "else"
}
}
// 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 [email protected]
, through Slack, or open an issue in the Github Repo.
Updated 5 months ago