diff --git a/cypress/integration/validation.js b/cypress/integration/validation.js
index 2161406..bd2073c 100644
--- a/cypress/integration/validation.js
+++ b/cypress/integration/validation.js
@@ -1,1026 +1,1026 @@
 /**
  * Copyright (C) 2020-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 /*
  * Tests the basic features of the application.
  */
 
 "use strict";
 
 describe('Document validation', function() {
     it('accepts empty document', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#name').should('have.value', '');
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('accepts all main fields', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "license": "https://spdx.org/licenses/AGPL-3.0",
                 "dateCreated": "2019-10-02",
                 "datePublished": "2020-01-01",
                 "name": "My Test Software",
                 "description": "This is a\ngreat piece of software",
             }))
         );
         cy.get('#validateCodemeta').click();
 
 
         cy.get('#name').should('have.value', '');
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('accepts anything in non-validated fields', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "programmingLanguage": "foo",
             }))
         );
         cy.get('#validateCodemeta').click();
         cy.get('#errorMessage').should('have.text', '');
 
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "programmingLanguage": 21,
             }))
         );
         cy.get('#validateCodemeta').click();
         cy.get('#errorMessage').should('have.text', '');
 
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "programmingLanguage": {},
             }))
         );
         cy.get('#validateCodemeta').click();
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('errors on invalid type', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "foo",
                 "name": "My Test Software",
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#name').should('have.value', '');
 
         cy.get('#errorMessage').should('have.text', 'Wrong document type: must be "SoftwareSourceCode"/"SoftwareApplication", not "foo"');
     });
 
     it('errors on invalid field name', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "foobar": "baz",
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#name').should('have.value', '');
 
         cy.get('#errorMessage').should('have.text', 'Unknown field "foobar".');
     });
 });
 
 describe('URLs validation', function() {
     it('accepts valid URL', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "codeRepository": "http://example.org/",
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('accepts empty list of URLs', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "codeRepository": [],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('accepts list of valid URLs', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "codeRepository": ["http://example.org/", "http://example.com/"],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('errors on invalid URL', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "codeRepository": "foo",
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', 'Invalid URL in field "codeRepository": "foo"');
     });
 
     it('errors on non-string instead of URL', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "codeRepository": {},
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"codeRepository" must be an URL (or a list of URLs), not: {}');
     });
 
     it('errors on list with an invalid URL at the end', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "codeRepository": ["http://example.org/", "foo"],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', 'Invalid URL in field "codeRepository": "foo"');
     });
 
     it('errors on list with an invalid URL at the beginning', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "codeRepository": ["http://example.org/", "foo"],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', 'Invalid URL in field "codeRepository": "foo"');
     });
 
     it('errors on non-string in URL list', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "codeRepository": ["http://example.org/", {}],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"codeRepository" must be a list of URLs (or a single URL), but it contains: {}');
     });
 });
 
 describe('Things or URLs validation', function() {
     it('accepts valid Thing', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "isPartOf": {
                     "@type": "SoftwareApplication",
                     "name": "Example Soft",
                 }
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('accepts valid URL', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "isPartOf": "http://example.org/",
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('accepts empty list of Things', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "isPartOf": [],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('accepts list of Things', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "isPartOf": [
                     {
                         "@type": "SoftwareApplication",
                         "name": "Example Soft",
                     },
                     {
                         "@type": "SoftwareApplication",
                         "name": "Test Soft",
                     }
                 ]
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('errors on non-URL string', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "license": "Copyright 2021 Myself",
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"license" must be an URL or a CreativeWork/SoftwareSourceCode/SoftwareApplication object, not: "Copyright 2021 Myself"');
     });
 
     it('errors on wrong type', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "isPartOf": 42,
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"isPartOf" must be a CreativeWork/SoftwareSourceCode/SoftwareApplication object or URI, not 42');
     });
 
     it('errors on non-Thing object', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "isPartOf": {},
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"isPartOf" must be a (list of) CreativeWork/SoftwareSourceCode/SoftwareApplication object(s) or an URI, but is missing a type/@type.');
     });
 
     it('errors on list with an invalid Thing at the beginning', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "isPartOf": [
                     {},
                     {
                         "@type": "SoftwareApplication",
                         "name": "Example Soft",
                     }
                 ],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"isPartOf" must be a (list of) CreativeWork/SoftwareSourceCode/SoftwareApplication object(s) or an URI, but is missing a type/@type.');
     });
 
     it('errors on list with an invalid Thing at the end', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "isPartOf": [
                     {
                         "@type": "SoftwareApplication",
                         "name": "Example Soft",
                     },
                     {}
                 ],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"isPartOf" must be a (list of) CreativeWork/SoftwareSourceCode/SoftwareApplication object(s) or an URI, but is missing a type/@type.');
     });
 });
 
 describe('Texts or URLs validation', function() {
     it('accepts valid Text', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "applicationCategory": "foo",
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('accepts valid URL', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "applicationCategory": "http://example.org/",
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('accepts empty list of Texts', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "applicationCategory": [],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('accepts list of Texts', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "applicationCategory": ["foo", "bar"],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('errors on non-string instead of Text', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "applicationCategory": {},
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"applicationCategory" must be a text/URL (or a list of texts/URLs), not: {}');
     });
 
     it('errors on list with an invalid Text at the beginning', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "applicationCategory": [{}, "foo"],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"applicationCategory" must be a list of texts/URLs (or a single text/URL), but it contains: {}');
     });
 
     it('errors on list with an invalid Text at the end', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "applicationCategory": ["foo", {}],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"applicationCategory" must be a list of texts/URLs (or a single text/URL), but it contains: {}');
     });
 });
 
 describe('Text validation', function() {
     it('accepts valid Text', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "description": "foo",
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('errors on empty list of Texts', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "description": [],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"description" must be text, not []');
     });
 
     it('errors on list of Texts', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "description": ["foo", "bar"],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"description" must be text, not ["foo","bar"]');
     });
 
     it('errors on non-string instead of Text', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "description": {},
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"description" must be text, not {}');
     });
 });
 
 describe('Date validation', function() {
     it('accepts valid Date', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "dateCreated": "2020-03-18",
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('errors on empty list of Dates', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "dateCreated": [],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"dateCreated" must be a date, not []');
     });
 
     it('errors on list of Dates', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "dateCreated": ["2020-03-18", "2020-03-19"],
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"dateCreated" must be a date, not ["2020-03-18","2020-03-19"]');
     });
 
     it('errors on non-string instead of Date', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "dateCreated": {},
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"dateCreated" must be a date, not {}');
     });
 
     it('errors on non-Date string', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "dateCreated": "foo",
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"dateCreated" must be a date in the format YYYY-MM-DD, not "foo"');
     });
 });
 
 describe('Person validation', function() {
     it('accepts URI', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": "http://example.org/~jdoe",
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('accepts valid complete Person', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": {
                     "@type": "Person",
                     "@id": "http://example.org/~jdoe",
                     "url": "http://example.org/~jdoe",
                     "name": "Jane Doe",
                     "givenName": "Jane",
                     "familyName": "Doe",
                     "email": "jdoe@example.org",
                     "affiliation": {
                         "@type": "Organization",
                         "@id": "http://example.org/",
                     }
                 }
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('errors on Person with missing type', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": {
                 },
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Role/Person/Organization object(s) or an URI, but is missing a type/@type.');
     });
 
     it('errors on Person with wrong type', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": {
                     "type": "SoftwareSourceCode",
                 },
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"author" type must be a (list of) Role/Person/Organization object(s), not "SoftwareSourceCode"');
     });
 
     it('errors on Person with unknown field', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": {
                     "type": "Person",
                     "foo": "bar",
                 },
             }))
         );
         cy.get('#validateCodemeta').click();
 
-        cy.get('#errorMessage').should('have.text', 'Unknown field "foo" in "author".');
+        cy.get('#errorMessage').should('have.text', 'Unknown field "foo".');
     });
 
     it('errors on Person with invalid field', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": {
                     "type": "Person",
                     "email": 32,
                 },
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"email" must be text, not 32');
     });
 
     it('accepts URI in list', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": [
                     "http://example.org/~jadoe",
                     {
                         "@type": "Person",
                         "@id": "http://example.org/~jodoe",
                         "givenName": "John",
                         "familyName": "Doe",
                     },
                 ]
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('accepts list of valid Person', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": [
                     {
                         "@type": "Person",
                         "@id": "http://example.org/~jadoe",
                         "givenName": "Jane",
                         "familyName": "Doe",
                     },
                     {
                         "@type": "Person",
                         "@id": "http://example.org/~jodoe",
                         "givenName": "John",
                         "familyName": "Doe",
                     },
                 ]
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('accepts Person with multiple affiliations', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": {
                     "@type": "Person",
                     "@id": "http://example.org/~jdoe",
                     "name": "Jane Doe",
                     "affiliation": [
                         {
                             "@type": "Organization",
                             "@id": "http://example.org/",
                         },
                         {
                             "@type": "Organization",
                             "@id": "http://example.com/",
                         },
                     ]
                 }
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('errors on list with invalid Person at the beginning', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": [
                     {
                         "givenName": "Jane",
                         "familyName": "Doe",
                     },
                     {
                         "@type": "Person",
                         "@id": "http://example.org/~jodoe",
                         "name": "John Doe",
                         "givenName": "John",
                         "familyName": "Doe",
                     },
                 ]
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Role/Person/Organization object(s) or an URI, but is missing a type/@type.');
     });
 
     it('errors on list with invalid Person at the end', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": [
                     {
                         "@type": "Person",
                         "@id": "http://example.org/~jadoe",
                         "givenName": "Jane",
                         "familyName": "Doe",
                     },
                     {
                         "givenName": "John",
                         "familyName": "Doe",
                     },
                 ]
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Role/Person/Organization object(s) or an URI, but is missing a type/@type.');
     });
 });
 
 describe('Organization validation', function() {
     it('accepts valid complete Organization', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": {
                     "@type": "Organization",
                     "@id": "http://example.org/",
                     "url": "https://example.org/",
                     "name": "Example Org",
                     "identifier": "http://example.org/",
                     "address": "Nowhere",
                 }
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('errors on Organization with missing type', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": {
                 },
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Role/Person/Organization object(s) or an URI, but is missing a type/@type.');
     });
 
     it('errors on Organization with wrong type', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": {
                     "type": "SoftwareSourceCode",
                 },
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"author" type must be a (list of) Role/Person/Organization object(s), not "SoftwareSourceCode"');
     });
 
     it('errors on Organization with unknown field', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": {
                     "type": "Organization",
                     "foo": "bar",
                 },
             }))
         );
         cy.get('#validateCodemeta').click();
 
-        cy.get('#errorMessage').should('have.text', 'Unknown field "foo" in "author".');
+        cy.get('#errorMessage').should('have.text', 'Unknown field "foo".');
     });
 
     it('errors on Organization with invalid field', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": {
                     "type": "Organization",
                     "email": 32,
                 },
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"email" must be text, not 32');
     });
 
     it('accepts list of valid Organization', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": [
                     {
                         "@type": "Organization",
                         "@id": "http://example.org/",
                         "name": "Example Org",
                     },
                     {
                         "@type": "Organization",
                         "@id": "http://example.org/~jodoe",
                         "name": "Example Org",
                     },
                 ]
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('errors on list with invalid Organization at the beginning', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": [
                     {
                         "name": "Example Org",
                     },
                     {
                         "@type": "Organization",
                         "name": "Example Org",
                     },
                 ]
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Role/Person/Organization object(s) or an URI, but is missing a type/@type.');
     });
 
     it('errors on list with invalid Organization at the end', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "author": [
                     {
                         "@type": "Organization",
                         "name": "Example Org",
                     },
                     {
                         "name": "Example Org",
                     },
                 ]
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Role/Person/Organization object(s) or an URI, but is missing a type/@type.');
     });
 });
 
 describe('CreativeWork validation', function() {
     it('accepts valid CreativeWork', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "name": "Small Software",
                 "isPartOf": {
                     "type": "CreativeWork",
                     "name": "Big Creative Work",
                     "author": "http://example.org/~jdoe",
                     "keywords": ["foo", "bar"],
                 }
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '');
     });
 
     it('errors on CreativeWork with missing type', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "isPartOf": {
                 }
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"isPartOf" must be a (list of) CreativeWork/SoftwareSourceCode/SoftwareApplication object(s) or an URI, but is missing a type/@type.');
     });
 
     it('errors on CreativeWork with wrong type', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "isPartOf": {
                     "type": "Person",
                 },
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"isPartOf" type must be a (list of) CreativeWork/SoftwareSourceCode/SoftwareApplication object(s), not "Person"');
     });
 
     it('errors on CreativeWork with unknown field', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "isPartOf": {
                     "type": "CreativeWork",
                     "foo": "bar",
                 },
             }))
         );
         cy.get('#validateCodemeta').click();
 
-        cy.get('#errorMessage').should('have.text', 'Unknown field "foo" in "isPartOf".');
+        cy.get('#errorMessage').should('have.text', 'Unknown field "foo".');
     });
 
     it('errors on CreativeWork with invalid field', function() {
         cy.get('#codemetaText').then((elem) =>
             elem.text(JSON.stringify({
                 "@context": "https://doi.org/10.5063/schema/codemeta-2.0",
                 "@type": "SoftwareSourceCode",
                 "isPartOf": {
                     "type": "CreativeWork",
                     "url": 32,
                 },
             }))
         );
         cy.get('#validateCodemeta').click();
 
         cy.get('#errorMessage').should('have.text', '"url" must be an URL (or a list of URLs), not: 32');
     });
 });
diff --git a/index.html b/index.html
index cda6dc8..9ea85b4 100644
--- a/index.html
+++ b/index.html
@@ -1,394 +1,395 @@
 <!doctype html>
 <!--
 Copyright (C) 2019-2020  The Software Heritage developers
 See the AUTHORS file at the top-level directory of this distribution
 License: GNU Affero General Public License version 3, or any later version
 See top-level LICENSE file for more information
 -->
 <html lang="en">
 <head>
     <meta charset="utf-8">
     <title>CodeMeta generator</title>
     <script src="./js/utils.js"></script>
     <script src="./js/fields_data.js"></script>
     <script src="./js/dynamic_form.js"></script>
     <script src="./js/codemeta_generation.js"></script>
+    <script src="./js/validation/utils.js"></script>
     <script src="./js/validation/primitives.js"></script>
     <script src="./js/validation/things.js"></script>
     <script src="./js/validation/index.js"></script>
     <link rel="stylesheet" type="text/css" href="./main.css">
     <link rel="stylesheet" type="text/css" href="./codemeta.css">
 </head>
 <body>
   <header>
     <h1>CodeMeta generator v3.0</h1>
   </header>
 
   <main>
 
     <p>Most fields are optional. Mandatory fields will be highlighted when generating Codemeta.</p>
 
     <noscript>
         <p id="noscriptError">
             This application requires Javascript to show dynamic fields in the form,
             and generate a JSON file; but your browser does not support Javascript.
             If you cannot use a browser with Javascript support, you can try
             <a href="https://codemeta.github.io/tools/">one of the other available tools</a>
             or write the codemeta.json file directly.
         </p>
     </noscript>
 
     <form id="inputForm">
         <fieldset id="fieldsetSoftwareItself" class="leafFieldset">
             <legend>The software itself</legend>
 
             <p title="The name of the software">
                 <label for="name">Name</label>
                 <input type="text" name="name" id="name" aria-describedby="name_descr"
                     placeholder="My Software" required="required" />
 
                 <span class="field-description" id="name_descr">the software title</span>
             </p>
 
             <p title="a brief description of the software">
                 <label for="description">Description</label>
                 <textarea rows="4" cols="50"
                     name="description" id="description"
                     placeholder="My Software computes ephemerides and orbit propagation. It has been developed from early ´80." ></textarea>
             </p>
 
 
             <p title="The date on which the software was created.">
                 <label for="dateCreated">Creation date</label>
                 <input type="text" name="dateCreated" id="dateCreated"
                     placeholder="YYYY-MM-DD" pattern="\d{4}-\d{2}-\d{2}" />
             </p>
 
             <p title="Date of first publication.">
                 <label for="datePublished">First release date</label>
                 <input type="text" name="datePublished" id="datePublished"
                     placeholder="YYYY-MM-DD" pattern="\d{4}-\d{2}-\d{2}" />
             </p>
 
             <p>
                 <label for="license">License(s)</label>
                 <input list="licenses" name="license" id="license"
                     aria-describedby="licenses_descr"> <!-- TODO: insert placeholder -->
 
                 <datalist id="licenses">
                 </datalist>
                 <!-- This datalist is be filled automatically -->
 
                 <br />
                 <span class="field-description" id="licenses_descr">from <a href="https://spdx.org/license-list">SPDX licence list</a></span>
 
                 <div id="selected-licenses">
                     <!-- This div is to be filled as the user selects licenses -->
                 </div>
             </p>
         </fieldset>
 
         <fieldset id="fieldsetDiscoverabilityAndCitation" class="leafFieldset">
             <legend>Discoverability and citation</legend>
 
             <p title="Unique identifier">
                 <label for="identifier">Unique identifier</label>
                 <input type="text" name="identifier" id="identifier"
                     placeholder="10.151.xxxxx" aria-describedby="identifier_descr" />
 
                 <br />
                 <span class="field-description" id="identifier_descr">
                     such as ISBNs, GTIN codes, UUIDs etc..  <a href="http://schema.org/identifier">http://schema.org/identifier</a>
                 </span>
             </p>
             <!-- TODO:define better
 
             I looked at the schema.org definition of identifier (https://schema.org/identifier),
             it can be text, url or PropertyValue.
             Used as follows in data representation with microdata:
 
             <div property="identifier" typeof="PropertyValue">
             <span property="propertyID">DOI</span>:
             <span property="value">10.151.xxxxx</span>
             </div>
             we can use that with identifier-type and identifier-value to have a clearer idea
             of what needs to be in the input.
 
             -->
 
             <p title="Type of the software application">
                 <label for="applicationCategory">Application category</label>
                 <input type="text" name="applicationCategory" id="applicationCategory"
                     placeholder="Astronomy"  />
             </p>
 
             <p title="Comma-separated list of keywords">
                 <label for="keywords">Keywords</label>
                 <input type="text" name="keywords" id="keywords"
                     placeholder="ephemerides, orbit, astronomy"  />
             </p>
 
             <p title="Funding / grant">
                 <label for="funding">Funding</label>
                 <input type="text" name="funding" id="funding" aria-describedby="funding_descr"
                     placeholder="PRA_2018_73"/>
 
                 <br />
                 <span class="field-description" id="funding_descr">grant funding software development</span>
             </p>
 
             <p title="Funding / organization">
                 <label for="funder">Funder</label>
                 <input type="text" name="funder" id="funder" aria-describedby="funder_descr"
                     placeholder="Università di Pisa"/>
 
                 <br />
                 <span class="field-description" id="funder_descr">organization funding software development</span>
             </p>
 
             Authors and contributors can be added below
         </fieldset>
 
         <fieldset id="fieldsetDevelopmentCommunity" class="leafFieldset">
             <legend>Development community / tools</legend>
 
             <p title="Link to the repository where the un-compiled, human readable code and related code is located (SVN, Git, GitHub, CodePlex, institutional GitLab instance, etc.).">
                 <label for="codeRepository">Code repository</label>
                 <input type="URL" name="codeRepository" id="codeRepository"
                     placeholder="git+https://github.com/You/RepoName.git" />
             </p>
 
             <p title="Link to continuous integration service (Travis-CI, Gitlab CI, etc.).">
                 <label for="contIntegration">Continuous integration</label>
                 <input type="URL" name="contIntegration" id="contIntegration"
                     placeholder="https://travis-ci.org/You/RepoName" />
             </p>
 
             <p title="Link to a place for users/developpers to report and manage bugs (JIRA, GitHub issues, etc.).">
                 <label for="issueTracker">Issue tracker</label>
                 <input type="URL" name="issueTracker" id="issueTracker"
                     placeholder="https://github.com/You/RepoName/issues" />
             </p>
 
             <p title="Related document, software, tools">
                 <label for="relatedLink">Related links</label>
                 <br />
                 <textarea rows="4" cols="50"
                     name="relatedLink" id="relatedLink"></textarea>
         </fieldset>
 
         <fieldset id="fieldsetRuntime" class="leafFieldset">
             <legend>Run-time environment</legend>
 
             <p title="Programming Languages, separated by commas">
                 <label for="programmingLanguage">Programming Language</label>
                 <input type="text" name="programmingLanguage" id="programmingLanguage"
                     placeholder="C#, Java, Python 3"  />
             </p>
 
             <p title="Runtime Platforms, separated by commas">
                 <label for="runtimePlatform">Runtime Platform</label>
                 <input type="text" name="runtimePlatform" id="runtimePlatform"
                     placeholder=".NET, JVM" />
             </p>
 
             <p title="Operating Systems, separated by commas">
                 <label for="operatingSystem">Operating System</label>
                 <input type="text" name="operatingSystem" id="operatingSystem"
                     placeholder="Android 1.6, Linux, Windows, macOS" />
             </p>
 
             <p title="Required software to run/use this one.">
                 <label for="softwareRequirements">Other software requirements</label>
                 <br />
                 <textarea rows="4" cols="50"
                     name="softwareRequirements" id="softwareRequirements"
                     placeholder=
 "Python 3.4
 https://github.com/psf/requests"></textarea>
         </fieldset>
 
         <fieldset id="fieldsetCurrentVersion" class="leafFieldset">
             <legend>Current version of the software</legend>
 
             <p title="Version number of the software">
                 <label for="version">Version number</label>
                 <input type="text" name="version" id="version"
                     placeholder="1.0.0" />
             </p>
 
             <p title="The date on which the software was most recently modified.">
                 <label for="dateModified">Release date</label>
                 <input type="text" name="dateModified" id="dateModified"
                     placeholder="YYYY-MM-DD" pattern="\d{4}-\d{2}-\d{2}" />
             </p>
 
             <p title="Download link">
                 <label for="downloadUrl">Download URL</label>
                 <input type="URL" name="downloadUrl" id="downloadUrl"
                     placeholder="https://example.org/MySoftware.tar.gz" />
             </p>
 
             <p title="a brief description of the software">
                 <label for="releaseNotes">Release notes</label>
                 <br />
                 <textarea rows="4" cols="50"
                     name="releaseNotes" id="releaseNotes"
                     placeholder=
 "Change log: this and that;
 Bugfixes: that and this." ></textarea>
             </p>
 
 <!--TODO: referencePublication as ScholarlyArticle array -->
 
         </fieldset>
 
         <fieldset id="review_container" class="leafFieldset">
             <legend>Editorial review</legend>
 
             <p title="Scholarly article describing this software">
                 <label for="referencePublication">Reference Publication</label>
                 <input type="URL" name="referencePublication" id="referencePublication"
                     placeholder="https://doi.org/10.1000/xyz123" />
             </p>
 
             <p title="Part or facet of the object being review ">
                 <label for="reviewAspect">Review aspect</label>
                 <input type="text" name="reviewAspect" id="reviewAspect"
                     placeholder="Object facet" />
             </p>
 
           <p title="The actual body of the review ">
                 <label for="reviewBody">Review body</label>
                 <textarea rows="4" cols="50"
                     name="reviewBody" id="reviewBody"
                     placeholder="Review about my software." ></textarea>
             </p>
         </fieldset>
 
         <fieldset id="fieldsetAdditionalInfo" class="leafFieldset">
             <legend>Additional Info</legend>
 
             <p title="Development Status">
             <label for="developmentStatus">Development Status</label>
                 <datalist id="developmentStatuses">
                         <option value="concept">
                         <option value="wip">
                         <option value="suspended">
                         <option value="abandoned">
                         <option value="active">
                         <option value="inactive">
                         <option value="unsupported">
                         <option value="moved">
                 </datalist>
                 <input list="developmentStatuses" id="developmentStatus" aria-describedby="developmentStatuses_descr"
                 pattern="concept|wip|suspended|abandoned|active|inactive|unsupported|moved">
 
                 <br />
                 <span class="field-description" id="developmentStatuses_descr">
                     see <a href="http://www.repostatus.org">www.repostatus.org</a> for details
                 </span>
             </p>
 
             <p title="Source Code of">
                 <label for="isSourceCodeOf">Is Source Code of</label>
                 <input type="text" name="isSourceCodeOf" id="isSourceCodeOf"
                     placeholder="Bigger Application" />
             </p>
 
             <p title="Part of">
                 <label for="isPartOf">Is part of</label>
                 <input type="URL" name="isPartOf" id="isPartOf"
                     placeholder="http://The.Bigger.Framework.org"  />
             </p>
         </fieldset>
 
         <div class="dynamicFields">
             <fieldset class="persons" id="author_container">
                 <legend>Authors</legend>
 
                 <input type="hidden" id="author_nb" value="0" />
                 <div id="addRemoveAuthor">
                     <input type="button" id="author_add" value="Add one"
                         onclick="addPerson('author', 'Author');" />
                     <input type="button" id="author_remove" value="Remove last"
                         onclick="removePerson('author');" />
                 </div>
             </fieldset>
 
             <fieldset class="persons" id="contributor_container">
                 <legend>Contributors</legend>
 
                 <p>Order of contributors does not matter.</p>
 
                 <input type="hidden" id="contributor_nb" value="0" />
                 <div id="addRemoveContributor">
                     <input type="button" id="contributor_add" value="Add one"
                         onclick="addPerson('contributor', 'Contributor');" />
                     <input type="button" id="contributor_remove" value="Remove last"
                         onclick="removePerson('contributor');" />
                 </div>
             </fieldset>
         </div>
 
     </form>
     <form>
       <input type="button" id="generateCodemetaV3" value="Generate codemeta.json v3.0" disabled
              title="Creates a codemeta.json v3.0 file below, from the information provided above." />
       <input type="button" id="generateCodemetaV2" value="Generate codemeta.json v2.0" disabled
             title="Creates a codemeta.json v2.0 file below, from the information provided above." />
         <input type="button" id="resetForm" value="Reset form"
             title="Erases all fields." />
         <input type="button" id="validateCodemeta" value="Validate codemeta.json" disabled
             title="Checks the codemeta.json file below is valid, and displays errors." />
         <input type="button" id="importCodemeta" value="Import codemeta.json" disabled
             title="Fills the fields above based on the codemeta.json file below." />
     </form>
 
     <p id="errorMessage">
     </p>
     <p>codemeta.json:</p>
     <pre contentEditable="true" id="codemetaText"></pre>
   </main>
 
   <footer>
     <p style="text-align:center;">
         Do you want to improve this tool ?
         Check out the
         <a href="https://github.com/codemeta/codemeta-generator">
             CodeMeta-generator repository</a>
         <br />
         Join the
         <a href="https://github.com/codemeta/codemeta">CodeMeta community</a>
         discussion
         <br />
         The CodeMeta vocabulary -
         <a href="https://doi.org/10.5063/schema/codemeta-2.0">v2.0</a>
         -
         <a href="https://w3id.org/codemeta/3.0">v3.0</a>
     </p>
 
     <h2 style="text-align:right;">Contributed by</h2>
     <p style="text-align:right;">
         <a href="https://www.softwareheritage.org/save-and-reference-research-software/">
             <img alt="Software Heritage" src="https://annex.softwareheritage.org/public/logo/software-heritage-logo-title-motto.svg"
                 width="300">
         </a>
     </p>
   </footer>
 
   <script src="./js/libs/jsonld/jsonld.min.js"></script>
   <script>
     Promise.all([loadSpdxData(), loadContextData()]).then(results => {
       const [licenses, contexts] = results;
       SPDX_LICENSES = licenses;
       SPDX_LICENSE_IDS = licenses.map(license => license['licenseId']);
 
       initJsonldLoader(contexts);
       initFields();
       initCallbacks();
       loadStateFromStorage();
     });
   </script>
 </body>
 </html>
diff --git a/js/codemeta_generation.js b/js/codemeta_generation.js
index 6dd111d..195e9a0 100644
--- a/js/codemeta_generation.js
+++ b/js/codemeta_generation.js
@@ -1,437 +1,439 @@
 /**
  * Copyright (C) 2019-2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 "use strict";
 
-const LOCAL_CONTEXT_PATH = "./data/contexts/codemeta-local.jsonld";
-const LOCAL_CONTEXT_URL = "local";
+const INTERNAL_CONTEXT_PATH = "./data/contexts/codemeta-local.jsonld";
+const INTERNAL_CONTEXT_URL = "internal";
 const CODEMETA_CONTEXTS = {
     "2.0": {
         path: "./data/contexts/codemeta-2.0.jsonld",
         url: "https://doi.org/10.5063/schema/codemeta-2.0"
     },
     "3.0": {
         path: "./data/contexts/codemeta-3.0.jsonld",
         url: "https://w3id.org/codemeta/3.0"
     }
 }
 
 const SPDX_PREFIX = 'https://spdx.org/licenses/';
 
 const loadContextData = async () => {
     const [contextLocal, contextV2, contextV3] =
         await Promise.all([
-            fetch(LOCAL_CONTEXT_PATH).then(response => response.json()),
+            fetch(INTERNAL_CONTEXT_PATH).then(response => response.json()),
             fetch(CODEMETA_CONTEXTS["2.0"].path).then(response => response.json()),
             fetch(CODEMETA_CONTEXTS["3.0"].path).then(response => response.json())
         ]);
     return {
-        [LOCAL_CONTEXT_URL]: contextLocal,
+        [INTERNAL_CONTEXT_URL]: contextLocal,
         [CODEMETA_CONTEXTS["2.0"].url]: contextV2,
         [CODEMETA_CONTEXTS["3.0"].url]: contextV3
     }
 }
 
 const getJsonldCustomLoader = contexts => {
     return url => {
         const xhrDocumentLoader = jsonld.documentLoaders.xhr();
         if (url in contexts) {
             return {
                 contextUrl: null,
                 document: contexts[url],
                 documentUrl: url
             };
         }
         return xhrDocumentLoader(url);
     }
 };
 
 const initJsonldLoader = contexts => {
     jsonld.documentLoader = getJsonldCustomLoader(contexts);
 };
 
 function emptyToUndefined(v) {
     if (v == null || v == "")
         return undefined;
     else
         return v;
 }
 
 function getIfSet(query) {
     return emptyToUndefined(document.querySelector(query).value);
 }
 
 function setIfDefined(query, value) {
     if (value !== undefined) {
         document.querySelector(query).value = value;
     }
 }
 
 function getLicenses() {
     let selectedLicenses = Array.from(document.getElementById("selected-licenses").children);
     return selectedLicenses.map(licenseDiv => SPDX_PREFIX + licenseDiv.children[0].innerText);
 }
 
 // Names of codemeta properties with a matching HTML field name
 const directCodemetaFields = [
     'codeRepository',
     'contIntegration',
     'dateCreated',
     'datePublished',
     'dateModified',
     'downloadUrl',
     'issueTracker',
     'name',
     'version',
     'identifier',
     'description',
     'applicationCategory',
     'releaseNotes',
     'funding',
     'developmentStatus',
     'isSourceCodeOf',
     'isPartOf',
     'referencePublication'
 ];
 
 const splittedCodemetaFields = [
     ['keywords', ','],
     ['programmingLanguage', ','],
     ['runtimePlatform', ','],
     ['operatingSystem', ','],
     ['softwareRequirements', '\n'],
     ['relatedLink', '\n'],
 ]
 
 // Names of codemeta properties with a matching HTML field name,
 // in a Person object
 const directPersonCodemetaFields = [
     'givenName',
     'familyName',
     'email',
     'affiliation',
 ];
 
 const directRoleCodemetaFields = [
     'roleName',
     'startDate',
     'endDate',
 ];
 
 const directReviewCodemetaFields = [
     'reviewAspect',
     'reviewBody'
 ];
 
 const crossCodemetaFields = {
     "contIntegration": ["contIntegration", "continuousIntegration"],
     // "embargoDate": ["embargoDate", "embargoEndDate"], Not present in the form yet TODO ?
 };
 
 function generateShortOrg(fieldName) {
     var affiliation = getIfSet(fieldName);
     if (affiliation !== undefined) {
         if (isUrl(affiliation)) {
             return {
                 "@type": "Organization",
                 "@id": affiliation,
             };
         }
         else {
             return {
                 "@type": "Organization",
                 "name": affiliation,
             };
         }
     }
     else {
         return undefined;
     }
 }
 
 function generatePerson(idPrefix) {
     var doc = {
         "@type": "Person",
     }
     var id = getIfSet(`#${idPrefix}_id`);
     if (id !== undefined) {
         doc["@id"] = id;
     }
     directPersonCodemetaFields.forEach(function (item, index) {
         doc[item] = getIfSet(`#${idPrefix}_${item}`);
     });
     doc["affiliation"] = generateShortOrg(`#${idPrefix}_affiliation`);
 
     return doc;
 }
 
 function generateRole(id) {
     const doc = {
         "@type": "Role"
     };
     directRoleCodemetaFields.forEach(function (item, index) {
         doc[item] = getIfSet(`#${id} .${item}`);
     });
     return doc;
 }
 
 function generateRoles(idPrefix, person) {
     const roles = [];
     const roleNodes = document.querySelectorAll(`ul[id^=${idPrefix}_role_`);
     roleNodes.forEach(roleNode => {
         const role = generateRole(roleNode.id);
         role["schema:author"] = person; // Prefix with "schema:" to prevent it from expanding into a list
         roles.push(role);
     });
     return roles;
 }
 
 function generatePersons(prefix) {
     var persons = [];
     var nbPersons = getNbPersons(prefix);
 
     for (let personId = 1; personId <= nbPersons; personId++) {
         const idPrefix = `${prefix}_${personId}`;
         const person = generatePerson(idPrefix);
         persons.push(person);
         const roles = generateRoles(idPrefix, person);
         if (roles.length > 0) {
             persons = persons.concat(roles);
         }
     }
 
     return persons;
 }
 
 function generateReview() {
     const doc = {
         "@type": "Review"
     };
     directReviewCodemetaFields.forEach(function (item, index) {
         doc[item] = getIfSet(`#${item}`);
     });
     return doc;
 }
 
 async function buildExpandedJson() {
     var doc = {
-        "@context": LOCAL_CONTEXT_URL,
+        "@context": INTERNAL_CONTEXT_URL,
         "@type": "SoftwareSourceCode",
     };
 
     let licenses = getLicenses();
     if (licenses.length > 0) {
         doc["license"] = licenses;
     }
 
     // Generate most fields
     directCodemetaFields.forEach(function (item, index) {
         doc[item] = getIfSet('#' + item)
     });
 
     doc["funder"] = generateShortOrg('#funder', doc["affiliation"]);
 
     const review = generateReview();
     if (review["reviewAspect"] || review["reviewBody"]) {
         doc["review"] = generateReview();
     }
 
     // Generate simple fields parsed simply by splitting
     splittedCodemetaFields.forEach(function (item, index) {
         const id = item[0];
         const separator = item[1];
         const value = getIfSet('#' + id);
         if (value !== undefined) {
             doc[id] = value.split(separator).map(trimSpaces);
         }
     });
 
     // Generate dynamic fields
     var authors = generatePersons('author');
     if (authors.length > 0) {
         doc["author"] = authors;
     }
     var contributors = generatePersons('contributor');
     if (contributors.length > 0) {
         doc["contributor"] = contributors;
     }
 
     for (const [key, items] of Object.entries(crossCodemetaFields)) {
         items.forEach(item => {
            doc[item] = doc[key];
         });
     }
     return await jsonld.expand(doc);
 }
 
 // v2.0 is still default version for generation, for now
 async function generateCodemeta(codemetaVersion = "2.0") {
     var inputForm = document.querySelector('#inputForm');
     var codemetaText, errorHTML;
+    let compacted;
 
     if (inputForm.checkValidity()) {
         const expanded = await buildExpandedJson();
-        const compacted = await jsonld.compact(expanded, CODEMETA_CONTEXTS[codemetaVersion].url);
+        compacted = await jsonld.compact(expanded, CODEMETA_CONTEXTS[codemetaVersion].url);
         codemetaText = JSON.stringify(compacted, null, 4);
         errorHTML = "";
     }
     else {
         codemetaText = "";
         errorHTML = "invalid input (see error above)";
         inputForm.reportValidity();
     }
 
     document.querySelector('#codemetaText').innerText = codemetaText;
     setError(errorHTML);
 
 
     // Run validator on the exported value, for extra validation.
     // If this finds a validation, it means there is a bug in our code (either
     // generation or validation), and the generation MUST NOT generate an
     // invalid codemeta file, regardless of user input.
-    if (codemetaText && !validateDocument(JSON.parse(codemetaText))) {
+    const isValid = codemetaText && (await parseAndValidateCodemeta(false));
+    if (!isValid) {
         alert('Bug detected! The data you wrote is correct; but for some reason, it seems we generated an invalid codemeta.json. Please report this bug at https://github.com/codemeta/codemeta-generator/issues/new and copy-paste the generated codemeta.json file. Thanks!');
     }
 
     if (codemetaText) {
         // For restoring the form state on page reload
         sessionStorage.setItem('codemetaText', codemetaText);
     }
 }
 
 // Imports a single field (name or @id) from an Organization.
 function importShortOrg(fieldName, doc) {
     if (doc !== undefined) {
         // Use @id if set, else use name
         setIfDefined(fieldName, doc["name"]);
         setIfDefined(fieldName, getDocumentId(doc));
     }
 }
 
 function importReview(doc) {
     if (doc !== undefined) {
         directReviewCodemetaFields.forEach(item => {
             setIfDefined('#' + item, doc[item]);
         });
     }
 }
 
 function authorsEqual(author1, author2) {
     // TODO should test more properties for equality?
     return author1.givenName === author2.givenName
         && author1.familyName === author2.familyName
         && author1.email === author2.email;
 }
 
 function getSingleAuthorsFromRoles(docs) {
     return docs.filter(doc => getDocumentType(doc) === "Role")
         .map(doc => doc["schema:author"])
         .reduce((authorSet, currentAuthor) => {
             const foundAuthor = authorSet.find(author => authorsEqual(author, currentAuthor));
             if (!foundAuthor) {
                 return authorSet.concat([currentAuthor]);
             } else {
                 return authorSet;
             }
         }, []);
 }
 
 function importRoles(personPrefix, roles) {
     roles.forEach(role => {
         const roleId = addRole(`${personPrefix}`);
         directRoleCodemetaFields.forEach(item => {
             setIfDefined(`#${personPrefix}_${item}_${roleId}`, role[item]);
         });
     });
 }
 
 function importPersons(prefix, legend, docs) {
     if (docs === undefined) {
         return;
     }
 
     const authors = docs.filter(doc => getDocumentType(doc) === "Person");
     const authorsFromRoles = getSingleAuthorsFromRoles(docs);
     const allAuthorDocs = authors.concat(authorsFromRoles)
         .reduce((authors, currentAuthor) => {
             if (!authors.find(author => authorsEqual(author, currentAuthor))) {
                 authors.push(currentAuthor);
             }
             return authors;
         }, []);
 
     allAuthorDocs.forEach(function (doc, index) {
         var personId = addPerson(prefix, legend);
 
         setIfDefined(`#${prefix}_${personId}_id`, getDocumentId(doc));
         directPersonCodemetaFields.forEach(function (item, index) {
             setIfDefined(`#${prefix}_${personId}_${item}`, doc[item]);
         });
 
         importShortOrg(`#${prefix}_${personId}_affiliation`, doc['affiliation']);
 
         const roles = docs.filter(currentDoc => getDocumentType(currentDoc) === "Role")
             .filter(currentDoc => authorsEqual(currentDoc["schema:author"], doc));
         importRoles(`${prefix}_${personId}`, roles);
     });
 }
 
 async function importCodemeta() {
     var inputForm = document.querySelector('#inputForm');
     var doc = await parseAndValidateCodemeta(false);
     resetForm();
 
     if (doc['license'] !== undefined) {
         if (typeof doc['license'] === 'string') {
             doc['license'] = [doc['license']];
         }
 
         doc['license'].forEach(l => {
             if (l.indexOf(SPDX_PREFIX) !== 0) { return; }
             let licenseId = l.substring(SPDX_PREFIX.length);
             insertLicenseElement(licenseId);
         });
     }
 
     directCodemetaFields.forEach(function (item, index) {
         setIfDefined('#' + item, doc[item]);
     });
     importShortOrg('#funder', doc["funder"]);
     importReview(doc["review"]);
 
     // Import simple fields by joining on their separator
     splittedCodemetaFields.forEach(function (item, index) {
         const id = item[0];
         const separator = item[1];
         let value = doc[id];
         if (value !== undefined) {
             if (Array.isArray(value)) {
                 value = value.join(separator);
             }
             setIfDefined('#' + id, value);
         }
     });
 
     for (const [key, items] of Object.entries(crossCodemetaFields)) {
         let value = "";
         items.forEach(item => {
            value = doc[item] || value;
         });
         setIfDefined(`#${key}`, value);
     }
 
     importPersons('author', 'Author', doc['author'])
     importPersons('contributor', 'Contributor', doc['contributor'])
 }
 
 function loadStateFromStorage() {
     var codemetaText = sessionStorage.getItem('codemetaText')
     if (codemetaText) {
         document.querySelector('#codemetaText').innerText = codemetaText;
         importCodemeta();
     }
 }
diff --git a/js/validation/index.js b/js/validation/index.js
index 4035f8c..fdfad61 100644
--- a/js/validation/index.js
+++ b/js/validation/index.js
@@ -1,96 +1,254 @@
 /**
  * Copyright (C) 2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 /*
  * Reads a Codemeta file and shows human-friendly errors on it.
  *
  * This validator intentionaly does not use a schema, in order to show errors
  * that are easy to understand for users with no understanding of JSON-LD.
  */
 
+const softwareFieldValidators = {
+    "@id": validateUrl,
+    "id": validateUrl,
 
-function validateDocument(doc) {
-    if (!Array.isArray(doc) && typeof doc != 'object') {
+    "codeRepository": validateUrls,
+    "programmingLanguage": noValidation,
+    "runtimePlatform": validateTexts,
+    "targetProduct": noValidation, // TODO: validate SoftwareApplication
+    "applicationCategory": validateTextsOrUrls,
+    "applicationSubCategory": validateTextsOrUrls,
+    "downloadUrl": validateUrls,
+    "fileSize": validateText,  // TODO
+    "installUrl": validateUrls,
+    "memoryRequirements": validateTextsOrUrls,
+    "operatingSystem": validateTexts,
+    "permissions": validateTexts,
+    "processorRequirements": validateTexts,
+    "releaseNotes": validateTextsOrUrls,
+    "softwareHelp": validateCreativeWorks,
+    "softwareRequirements": noValidation, // TODO: validate SoftwareSourceCode
+    "softwareVersion": validateText, // TODO?
+    "storageRequirements": validateTextsOrUrls,
+    "supportingData": noValidation, // TODO
+    "author": validateActors,
+    "citation": validateCreativeWorks, // TODO
+    "contributor": validateActors,
+    "copyrightHolder": validateActors,
+    "copyrightYear": validateNumbers,
+    "creator": validateActors, // TODO: still in codemeta 2.0, but removed from master
+    "dateCreated": validateDate,
+    "dateModified": validateDate,
+    "datePublished": validateDate,
+    "editor": validatePersons,
+    "encoding": noValidation,
+    "fileFormat": validateTextsOrUrls,
+    "funder": validateActors, // TODO: may be other types
+    "keywords": validateTexts,
+    "license": validateCreativeWorks,
+    "producer": validateActors,
+    "provider": validateActors,
+    "publisher": validateActors,
+    "sponsor": validateActors,
+    "version": validateNumberOrText,
+    "isAccessibleForFree": validateBoolean,
+    "isSourceCodeOf": validateTextsOrUrls,
+    "isPartOf": validateCreativeWorks,
+    "hasPart": validateCreativeWorks,
+    "position": noValidation,
+    "identifier": noValidation, // TODO
+    "description": validateText,
+    "name": validateText,
+    "sameAs": validateUrls,
+    "url": validateUrls,
+    "relatedLink": validateUrls,
+    "review": validateReview,
+
+    "softwareSuggestions": noValidation, // TODO: validate SoftwareSourceCode
+    "maintainer": validateActors,
+    "contIntegration": validateUrls,
+    "continuousIntegration": validateUrls,
+    "buildInstructions": validateUrls,
+    "developmentStatus": validateText, // TODO: use only repostatus strings?
+    "embargoDate": validateDate,
+    "embargoEndDate": validateDate,
+    "funding": validateText,
+    "issueTracker": validateUrls,
+    "referencePublication": noValidation, // TODO?
+    "readme": validateUrls,
+};
+
+const creativeWorkFieldValidators = {
+    "@id": validateUrl,
+    "id": validateUrl,
+
+    "author": validateActors,
+    "citation": validateCreativeWorks, // TODO
+    "contributor": validateActors,
+    "copyrightHolder": validateActors,
+    "copyrightYear": validateNumbers,
+    "creator": validateActors, // TODO: still in codemeta 2.0, but removed from master
+    "dateCreated": validateDate,
+    "dateModified": validateDate,
+    "datePublished": validateDate,
+    "editor": validatePersons,
+    "encoding": noValidation,
+    "funder": validateActors, // TODO: may be other types
+    "keywords": validateTexts,
+    "license": validateCreativeWorks,
+    "producer": validateActors,
+    "provider": validateActors,
+    "publisher": validateActors,
+    "sponsor": validateActors,
+    "version": validateNumberOrText,
+    "isAccessibleForFree": validateBoolean,
+    "isPartOf": validateCreativeWorks,
+    "hasPart": validateCreativeWorks,
+    "position": noValidation,
+    "identifier": noValidation, // TODO
+    "description": validateText,
+    "name": validateText,
+    "sameAs": validateUrls,
+    "url": validateUrls,
+};
+
+const roleFieldValidators = {
+    "roleName": validateText,
+    "startDate": validateDate,
+    "endDate": validateDate,
+
+    "schema:author": validateActor
+};
+
+const personFieldValidators = {
+    "@id": validateUrl,
+    "id": validateUrl,
+
+    "givenName": validateText,
+    "familyName": validateText,
+    "email": validateText,
+    "affiliation": validateOrganizations,
+    "identifier": validateUrls,
+    "name": validateText,  // TODO: this is technically valid, but should be allowed here?
+    "url": validateUrls,
+};
+
+const organizationFieldValidators = {
+    "@id": validateUrl,
+    "id": validateUrl,
+
+    "email": validateText,
+    "identifier": validateUrls,
+    "name": validateText,
+    "address": validateText,
+    "sponsor": validateActors,
+    "funder": validateActors, // TODO: may be other types
+    "isPartOf": validateOrganizations,
+    "url": validateUrls,
+
+    // TODO: add more?
+};
+
+const reviewFieldValidators = {
+    "reviewAspect": validateText,
+    "reviewBody": validateText,
+}
+
+function switchCodemetaContext(codemetaJSON, contextUrl) {
+    const previousCodemetaContext = codemetaJSON["@context"];
+    codemetaJSON["@context"] = contextUrl;
+    return previousCodemetaContext;
+}
+
+async function validateTerms(codemetaJSON) {
+    try {
+        await jsonld.expand(codemetaJSON, { safe: true });
+    } catch (validationError) {
+        if (validationError.details.event.code === "invalid property") {
+            setError(`Unknown field "${validationError.details.event.details.property}".`);
+            return false;
+        }
+    }
+    return true;
+}
+
+function validateCodemetaJSON(codemetaJSON) {
+    if (!Array.isArray(codemetaJSON) && typeof codemetaJSON != 'object') {
         setError("Document must be an object (starting and ending with { and }), not ${typeof doc}.")
         return false;
     }
     // TODO: validate id/@id
 
     // TODO: check there is either type or @type but not both
-    var type = getDocumentType(doc);
+    var type = getDocumentType(codemetaJSON);
     if (type === undefined) {
         setError("Missing type (must be SoftwareSourceCode or SoftwareApplication).")
         return false;
     }
     else if (!isCompactTypeEqual(type, "SoftwareSourceCode") && !isCompactTypeEqual(type, "SoftwareApplication")) {
         // Check this before other fields, as a wrong type error is more
         // understandable than "invalid field".
         setError(`Wrong document type: must be "SoftwareSourceCode"/"SoftwareApplication", not ${JSON.stringify(type)}`)
         return false;
     }
-    else {
-        return Object.entries(doc).every((entry) => {
-            var fieldName = entry[0];
-            var subdoc = entry[1];
-            if (fieldName == "@context") {
-                // Was checked before
-                return true;
-            }
-            else if (fieldName == "type" || fieldName == "@type") {
-                // Was checked before
-                return true;
-            }
-            else if (isFieldFromOtherVersionToIgnore(fieldName)) {
-                // Do not check fields from other versions FIXME
-                return true;
-            }
-            else {
-                var validator = softwareFieldValidators[fieldName];
-                if (validator === undefined) {
-                    // TODO: find if it's a field that belongs to another type,
-                    // and suggest that to the user
-                    setError(`Unknown field "${fieldName}".`)
-                    return false;
-                }
-                else {
-                    return validator(fieldName, subdoc);
-                }
+    return true;
+}
+
+function validateDocument(doc) {
+    return Object.entries(doc)
+        .filter(([fieldName]) => !isKeyword(fieldName))
+        .every(([fieldName, subdoc]) => {
+            const compactedFieldName = getCompactType(fieldName);
+            var validator = softwareFieldValidators[compactedFieldName];
+            if (validator === undefined) {
+                // TODO: find if it's a field that belongs to another type,
+                // and suggest that to the user
+                setError(`Unknown field "${compactedFieldName}".`)
+                return false;
+            } else {
+                return validator(compactedFieldName, subdoc);
             }
         });
-    }
 }
 
-
 async function parseAndValidateCodemeta(showPopup) {
     var codemetaText = document.querySelector('#codemetaText').innerText;
-    let parsed, doc;
+    let parsed;
 
     try {
         parsed = JSON.parse(codemetaText);
     }
     catch (e) {
         setError(`Could not read codemeta document because it is not valid JSON (${e}). Check for missing or extra quote, colon, or bracket characters.`);
         return;
     }
 
     setError("");
 
-    var isValid = validateDocument(parsed);
+    let isJSONValid = validateCodemetaJSON(parsed);
+
+    const previousCodemetaContext = switchCodemetaContext(parsed, INTERNAL_CONTEXT_URL);
+
+    let areTermsValid = await validateTerms(parsed);
+
+    const expanded = await jsonld.expand(parsed);
+    const doc = await jsonld.compact(expanded, INTERNAL_CONTEXT_URL);
+
+    switchCodemetaContext(parsed, previousCodemetaContext)
+
+    let isDocumentValid = validateDocument(doc);
     if (showPopup) {
-        if (isValid) {
+        if (isJSONValid && areTermsValid && isDocumentValid) {
             alert('Document is valid!')
         }
         else {
             alert('Document is invalid.');
         }
     }
 
-    parsed["@context"] = LOCAL_CONTEXT_URL;
-    const expanded = await jsonld.expand(parsed);
-    doc = await jsonld.compact(expanded, LOCAL_CONTEXT_URL);
     return doc;
 }
diff --git a/js/validation/primitives.js b/js/validation/primitives.js
index 2dc1cd0..6488d14 100644
--- a/js/validation/primitives.js
+++ b/js/validation/primitives.js
@@ -1,146 +1,150 @@
 /**
  * Copyright (C) 2020  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 /*
  * Validators for native schema.org data types.
  */
 
+function noValidation(fieldName, doc) {
+    return true;
+}
+
 // Validates an URL or an array of URLs
 function validateUrls(fieldName, doc) {
     return validateListOrSingle(fieldName, doc, (subdoc, inList) => {
         if (typeof subdoc != 'string') {
             if (inList) {
                 setError(`"${fieldName}" must be a list of URLs (or a single URL), but it contains: ${JSON.stringify(subdoc)}`);
             }
             else {
                 setError(`"${fieldName}" must be an URL (or a list of URLs), not: ${JSON.stringify(subdoc)}`);
             }
             return false;
         }
         else {
             return validateUrl(fieldName, subdoc);
         }
     });
 }
 
 // Validates a single URL
 function validateUrl(fieldName, doc) {
     if (!isUrl(doc)) {
         setError(`Invalid URL in field "${fieldName}": ${JSON.stringify(doc)}`)
         return false;
     }
     else {
         return true;
     }
 }
 
 // Validates a Text/URL or an array of Texts/URLs
 function validateTextsOrUrls(fieldName, doc) {
     return validateListOrSingle(fieldName, doc, (subdoc, inList) => {
         if (typeof subdoc != 'string') {
             if (inList) {
                 setError(`"${fieldName}" must be a list of texts/URLs (or a single text/URL), but it contains: ${JSON.stringify(subdoc)}`);
             }
             else {
                 setError(`"${fieldName}" must be a text/URL (or a list of texts/URLs), not: ${JSON.stringify(subdoc)}`);
             }
             return false;
         }
         else {
             return true;
         }
     });
 }
 
 // Validates a Text or an array of Texts
 function validateTexts(fieldName, doc) {
     return validateListOrSingle(fieldName, doc, (subdoc, inList) => {
         if (typeof subdoc != 'string') {
             if (inList) {
                 setError(`"${fieldName}" must be a list of texts (or a single text), but it contains: ${JSON.stringify(subdoc)}`);
             }
             else {
                 setError(`"${fieldName}" must be a text (or a list of texts), not: ${JSON.stringify(subdoc)}`);
             }
             return false;
         }
         else {
             return true;
         }
     });
 }
 
 // Validates a single Text
 function validateText(fieldName, doc) {
     if (typeof doc != 'string') {
         setError(`"${fieldName}" must be text, not ${JSON.stringify(doc)}`);
         return false;
     }
     else {
         return true;
     }
 }
 
 // Validates a Number or list of Number
 function validateNumbers(fieldName, doc) {
     return validateListOrSingle(fieldName, doc, (subdoc, inList) => {
         if (typeof subdoc != 'number') {
             if (inList) {
                 setError(`"${fieldName}" must be an array of numbers (or a single number), but contains: ${JSON.stringify(subdoc)}`);
             }
             else {
                 setError(`"${fieldName}" must be a number or an array of numbers, not: ${JSON.stringify(subdoc)}`);
             }
             return false;
         }
         else {
             return true;
         }
     });
 }
 
 // Validates a single Text or Number
 function validateNumberOrText(fieldName, doc) {
     if (typeof doc == 'string') {
         return true;
     }
     else if (typeof doc == 'number') {
         return true;
     }
     else {
         setError(`"${fieldName}" must be text or a number, not ${JSON.stringify(doc)}`);
         return false;
     }
 }
 
 // Validates a single Boolean
 function validateBoolean(fieldName, doc) {
     if (typeof doc != 'boolean') {
         setError(`"${fieldName}" must be a boolean (ie. "true" or "false"), not ${JSON.stringify(subdoc)}`);
         return false;
     }
     else {
         return true;
     }
 }
 
 // Validates a single Date
 function validateDate(fieldName, doc) {
     let re = /^\d{4}-\d{2}-\d{2}$/;
     if (typeof doc != 'string') {
         setError(`"${fieldName}" must be a date, not ${JSON.stringify(doc)}`);
         return false;
     }
     else if (!doc.match(re)) {
         setError(`"${fieldName}" must be a date in the format YYYY-MM-DD, not ${JSON.stringify(doc)}`);
         return false;
     }
     else {
         return true;
     }
 }
 
diff --git a/js/validation/things.js b/js/validation/things.js
index c790bb0..542b5bb 100644
--- a/js/validation/things.js
+++ b/js/validation/things.js
@@ -1,344 +1,158 @@
 /**
  * Copyright (C) 2020-2021  The Software Heritage developers
  * See the AUTHORS file at the top-level directory of this distribution
  * License: GNU Affero General Public License version 3, or any later version
  * See top-level LICENSE file for more information
  */
 
 /*
  * Validators for codemeta objects derived from http://schema.org/Thing.
  */
 
-function getDocumentType(doc) {
-    // TODO: check there is at most one.
-    // FIXME: is the last variant allowed?
-    return doc["type"] || doc["@type"] || doc["codemeta:type"]
-}
-
-function getDocumentId(doc) {
-    return doc["id"] || doc["@id"];
-}
-
-function isCompactTypeEqual(type, compactedType) {
-    // FIXME: are all variants allowed?
-    return (type == `${compactedType}`
-        || type == `schema:${compactedType}`
-        || type == `codemeta:${compactedType}`
-        || type == `http://schema.org/${compactedType}`
-    );
-}
-
-function isFieldFromOtherVersionToIgnore(fieldName) {
-    return ["codemeta:contIntegration", "codemeta:continuousIntegration", "codemeta:isSourceCodeOf",
-        "schema:review", "schema:reviewAspect", "schema:reviewBody"].includes(fieldName);
-}
-
-function noValidation(fieldName, doc) {
-    return true;
-}
-
-
 // Validates subtypes of Thing, or URIs
 //
 // typeFieldValidators is a map: {type => {fieldName => fieldValidator}}
 function validateThingOrId(parentFieldName, typeFieldValidators, doc) {
     var acceptedTypesString = Object.keys(typeFieldValidators).join('/');
 
     if (typeof doc == 'string') {
         if (!isUrl(doc)) {
             setError(`"${parentFieldName}" must be an URL or a ${acceptedTypesString} object, not: ${JSON.stringify(doc)}`);
             return false;
         }
         else {
             return true;
         }
     }
     else if (!Array.isArray(doc) && typeof doc == 'object') {
         return validateThing(parentFieldName, typeFieldValidators, doc);
     }
     else {
         setError(`"${parentFieldName}" must be a ${acceptedTypesString} object or URI, not ${JSON.stringify(doc)}`);
         return false;
     }
 }
 
 // Validates subtypes of Thing
 //
 // typeFieldValidators is a map: {type => {fieldName => fieldValidator}}
 function validateThing(parentFieldName, typeFieldValidators, doc) {
     // TODO: check there is either id or @id but not both
     // TODO: check there is either type or @type but not both
 
     var acceptedTypesString = Object.keys(typeFieldValidators).join('/');
 
     var documentType = getDocumentType(doc);
 
     var id = getDocumentId(doc);
     if (id !== undefined && !isUrl(id)) {
         setError(`"${fieldName}" has an invalid URI as id: ${JSON.stringify(id)}"`);
         return false;
     }
 
     if (documentType === undefined) {
         if (id === undefined) {
             setError(`"${parentFieldName}" must be a (list of) ${acceptedTypesString} object(s) or an URI, but is missing a type/@type.`);
             return false;
         }
         else {
             // FIXME: we have an @id but no @type, what should we do?
             return true;
         }
     }
 
     for (expectedType in typeFieldValidators) {
         if (isCompactTypeEqual(documentType, expectedType)) {
             var fieldValidators = typeFieldValidators[expectedType];
-            return Object.entries(doc).every((entry) => {
-                var fieldName = entry[0];
-                var subdoc = entry[1];
-                if (fieldName == "type" || fieldName == "@type") {
-                    // Was checked before
-                    return true;
-                }
-                else if (isFieldFromOtherVersionToIgnore(fieldName)) {
-                    // Do not check fields from other versions FIXME
-                    return true;
-                }
-                else {
-                    var validator = fieldValidators[fieldName];
+
+            return Object.entries(doc)
+                .filter(([fieldName]) => !isKeyword(fieldName))
+                .every(([fieldName, subdoc]) => {
+                    const compactedFieldName = getCompactType(fieldName);
+                    var validator = fieldValidators[compactedFieldName];
                     if (validator === undefined) {
                         // TODO: find if it's a field that belongs to another type,
                         // and suggest that to the user
-                        setError(`Unknown field "${fieldName}" in "${parentFieldName}".`)
+                        setError(`Unknown field "${compactedFieldName}".`)
                         return false;
+                    } else {
+                        return validator(compactedFieldName, subdoc);
                     }
-                    else {
-                        return validator(fieldName, subdoc);
-                    }
-                }
-            });
+                });
         }
     }
 
     setError(`"${parentFieldName}" type must be a (list of) ${acceptedTypesString} object(s), not ${JSON.stringify(documentType)}`);
     return false;
 }
 
 
 // Helper function to validate a field is either X or a list of X.
 function validateListOrSingle(fieldName, doc, validator) {
     if (Array.isArray(doc)) {
         return doc.every((subdoc) => validator(subdoc, true));
     }
     else {
         return validator(doc, false);
     }
 }
 
 // Validates a CreativeWork or an array of CreativeWork
 function validateCreativeWorks(fieldName, doc) {
     return validateListOrSingle(fieldName, doc, (subdoc, inList) => {
         return validateCreativeWork(fieldName, subdoc);
     });
 }
 
 // Validates a single CreativeWork
 function validateCreativeWork(fieldName, doc) {
     return validateThingOrId(fieldName, {
         "CreativeWork": creativeWorkFieldValidators,
         "SoftwareSourceCode": softwareFieldValidators,
         "SoftwareApplication": softwareFieldValidators,
     }, doc);
 }
 
 // Validates a Person, Organization or an array of these
 function validateActors(fieldName, doc) {
     return validateListOrSingle(fieldName, doc, (subdoc, inList) => {
         return validateActor(fieldName, subdoc);
     });
 }
 
 // Validates a Person or an array of Person
 function validatePersons(fieldName, doc) {
     return validateListOrSingle(fieldName, doc, (subdoc, inList) => {
         return validatePerson(fieldName, subdoc);
     });
 }
 
 // Validates an Organization or an array of Organization
 function validateOrganizations(fieldName, doc) {
     return validateListOrSingle(fieldName, doc, (subdoc, inList) => {
         return validateOrganization(fieldName, subdoc);
     });
 }
 
 // Validates a single Person or Organization
 function validateActor(fieldName, doc) {
     return validateThingOrId(fieldName, {
         "Role": roleFieldValidators,
         "Person": personFieldValidators,
         "Organization": organizationFieldValidators,
     }, doc);
 }
 
 // Validates a single Person object
 function validatePerson(fieldName, doc) {
     return validateThingOrId(fieldName, {"Person": personFieldValidators}, doc);
 }
 
 // Validates a single Organization object
 function validateOrganization(fieldName, doc) {
     return validateThingOrId(fieldName, {"Organization": organizationFieldValidators}, doc);
 }
 
 function validateReview(fieldName, doc) {
     return validateThingOrId(fieldName, {"Review": reviewFieldValidators}, doc);
 }
-
-
-var softwareFieldValidators = {
-    "@id": validateUrl,
-    "id": validateUrl,
-
-    "codeRepository": validateUrls,
-    "programmingLanguage": noValidation,
-    "runtimePlatform": validateTexts,
-    "targetProduct": noValidation, // TODO: validate SoftwareApplication
-    "applicationCategory": validateTextsOrUrls,
-    "applicationSubCategory": validateTextsOrUrls,
-    "downloadUrl": validateUrls,
-    "fileSize": validateText,  // TODO
-    "installUrl": validateUrls,
-    "memoryRequirements": validateTextsOrUrls,
-    "operatingSystem": validateTexts,
-    "permissions": validateTexts,
-    "processorRequirements": validateTexts,
-    "releaseNotes": validateTextsOrUrls,
-    "softwareHelp": validateCreativeWorks,
-    "softwareRequirements": noValidation, // TODO: validate SoftwareSourceCode
-    "softwareVersion": validateText, // TODO?
-    "storageRequirements": validateTextsOrUrls,
-    "supportingData": noValidation, // TODO
-    "author": validateActors,
-    "citation": validateCreativeWorks, // TODO
-    "contributor": validateActors,
-    "copyrightHolder": validateActors,
-    "copyrightYear": validateNumbers,
-    "creator": validateActors, // TODO: still in codemeta 2.0, but removed from master
-    "dateCreated": validateDate,
-    "dateModified": validateDate,
-    "datePublished": validateDate,
-    "editor": validatePersons,
-    "encoding": noValidation,
-    "fileFormat": validateTextsOrUrls,
-    "funder": validateActors, // TODO: may be other types
-    "keywords": validateTexts,
-    "license": validateCreativeWorks,
-    "producer": validateActors,
-    "provider": validateActors,
-    "publisher": validateActors,
-    "sponsor": validateActors,
-    "version": validateNumberOrText,
-    "isAccessibleForFree": validateBoolean,
-    "isSourceCodeOf": validateTextsOrUrls,
-    "isPartOf": validateCreativeWorks,
-    "hasPart": validateCreativeWorks,
-    "position": noValidation,
-    "identifier": noValidation, // TODO
-    "description": validateText,
-    "name": validateText,
-    "sameAs": validateUrls,
-    "url": validateUrls,
-    "relatedLink": validateUrls,
-    "review": validateReview,
-
-    "softwareSuggestions": noValidation, // TODO: validate SoftwareSourceCode
-    "maintainer": validateActors,
-    "contIntegration": validateUrls,
-    "continuousIntegration": validateUrls,
-    "buildInstructions": validateUrls,
-    "developmentStatus": validateText, // TODO: use only repostatus strings?
-    "embargoDate": validateDate,
-    "embargoEndDate": validateDate,
-    "funding": validateText,
-    "issueTracker": validateUrls,
-    "referencePublication": noValidation, // TODO?
-    "readme": validateUrls,
-};
-
-var creativeWorkFieldValidators = {
-    "@id": validateUrl,
-    "id": validateUrl,
-
-    "author": validateActors,
-    "citation": validateCreativeWorks, // TODO
-    "contributor": validateActors,
-    "copyrightHolder": validateActors,
-    "copyrightYear": validateNumbers,
-    "creator": validateActors, // TODO: still in codemeta 2.0, but removed from master
-    "dateCreated": validateDate,
-    "dateModified": validateDate,
-    "datePublished": validateDate,
-    "editor": validatePersons,
-    "encoding": noValidation,
-    "funder": validateActors, // TODO: may be other types
-    "keywords": validateTexts,
-    "license": validateCreativeWorks,
-    "producer": validateActors,
-    "provider": validateActors,
-    "publisher": validateActors,
-    "sponsor": validateActors,
-    "version": validateNumberOrText,
-    "isAccessibleForFree": validateBoolean,
-    "isPartOf": validateCreativeWorks,
-    "hasPart": validateCreativeWorks,
-    "position": noValidation,
-    "identifier": noValidation, // TODO
-    "description": validateText,
-    "name": validateText,
-    "sameAs": validateUrls,
-    "url": validateUrls,
-};
-
-var roleFieldValidators = {
-    "roleName": validateText,
-    "startDate": validateDate,
-    "endDate": validateDate,
-
-    "schema:author": validateActor
-};
-
-var personFieldValidators = {
-    "@id": validateUrl,
-    "id": validateUrl,
-
-    "givenName": validateText,
-    "familyName": validateText,
-    "email": validateText,
-    "affiliation": validateOrganizations,
-    "identifier": validateUrls,
-    "name": validateText,  // TODO: this is technically valid, but should be allowed here?
-    "url": validateUrls,
-};
-
-
-var organizationFieldValidators = {
-    "@id": validateUrl,
-    "id": validateUrl,
-
-    "email": validateText,
-    "identifier": validateUrls,
-    "name": validateText,
-    "address": validateText,
-    "sponsor": validateActors,
-    "funder": validateActors, // TODO: may be other types
-    "isPartOf": validateOrganizations,
-    "url": validateUrls,
-
-    // TODO: add more?
-};
-
-const reviewFieldValidators = {
-    "reviewAspect": validateText,
-    "reviewBody": validateText,
-}
diff --git a/js/validation/utils.js b/js/validation/utils.js
new file mode 100644
index 0000000..4f91c86
--- /dev/null
+++ b/js/validation/utils.js
@@ -0,0 +1,35 @@
+/**
+ * Copyright (C) 2020-2021  The Software Heritage developers
+ * See the AUTHORS file at the top-level directory of this distribution
+ * License: GNU Affero General Public License version 3, or any later version
+ * See top-level LICENSE file for more information
+ */
+
+function getDocumentType(doc) {
+    // TODO: check there is at most one.
+    // FIXME: is the last variant allowed?
+    return doc["type"] || doc["@type"] || doc["codemeta:type"]
+}
+
+function getDocumentId(doc) {
+    return doc["id"] || doc["@id"];
+}
+
+function getCompactType(type) {
+    return type
+        .replace("schema:", "")
+        .replace("codemeta:", "");
+}
+
+function isCompactTypeEqual(type, compactedType) {
+    // FIXME: are all variants allowed?
+    return (type == `${compactedType}`
+        || type == `schema:${compactedType}`
+        || type == `codemeta:${compactedType}`
+        || type == `http://schema.org/${compactedType}`
+    );
+}
+
+function isKeyword(term) {
+    return ["@context", "type"].includes(term);
+}