Object schema

For convenience, the SDK does its best to try to guess your desired outcome when saving and retrieving documents. It tries to parse known data types into native JavaScript types and vice-versa. But sometimes there is not enough data for the SDK to correctly guess the expected behavior. To make sure the SDK behaves as expected, a web application can give more information about properties and objects to the SDK through a context’s object schema.

Any context has an object schema that can be extended through the extendObjectSchema method.

Even though the information stored in the platform can seem to be normal JSON, it is not. If you haven’t read about the Essential Concepts of Linked Data don’t worry, you won’t need them to understand the basics behind object schemas, but it wouldn’t hurt you to read them either.

The platform stores each document as a set of statements (or RDF triples). These statements together comprise an entity or resource. For example, the following object:

var project = {
    name: "Project 1",
    labels: [
        "critical",
        "hr",
        "example"
    ],
    createdOn: new Date( 2016, 9, 25 )
};

Would be saved as statements that can be read:

  • project has the nameProject 1 (which is a string)
  • project has the labelcritical (which is a string)
  • project has the labelhr (which is a string)
  • project has the labelexample (which is a string)
  • project was createdOn2016/09/25 (which is a date)

There are a couple of things to notice here:

  • The platform saves the type of each property’s value
  • It doesn’t save information about the value of the property label being an array
  • The statements aren’t ordered

Because of this there are several common pitfalls that you can fall into when developing web applications with Carbon:

  • If you save a document with a property that has an array with only one value, and later on you retrieve the document, the property will point to the value directly. It won’t be in an array.
  • Any application that depends on an array keeping its order will behave randomly.
  • If your Carbon application is being used by several web applications, a web application could save statements about a property that other web applications don’t expect (nor understand).

All of these pitfalls can be overcome by using Object Schemas.

What is an object schema?

An object schema provides information about the properties the SDK can expect. On it, a web application can force the SDK to treat values as arrays, force arrays to be ordered, tell the SDK to ignore values of unknown types, etc.

Object schemas are stored inside of a context. Each context has it’s own object schema, and child contexts inherit their object schema from their parent’s.

To provide new information to the object schema the context.extendObjectSchema method can be used:

appContext.extendObjectSchema( {
    // New information
} );

The object passed to the extendObjectSchema method is a @context object defined in the JSON-LD specification. If you aren’t familiar with that specification, don’t worry. You don’tneed to understand the specification to be able to use object schemas, but again, the more information you posses, the better.

Properties inside of the object passed to extendObjectSchema will describe different aspects of the schema your application will use.

Properties

To provide information about the properties your application objects will have, a property with the same name needs to be defined on the object schema:

appContext.extendObjectSchema( {
    "labels": {
        // Metadata of the "labels" property
    }
} );

Collections (@container)

Properties by default are assumed to be one value only unless proven otherwise. That’s why if you save a document with a property that holds an array with one value, once you retrieve it you’ll receive only the value, not the array.

To specify that a property is always going to hold an array, you can declare the @container property on the object that describes the property.

That property will need to have one of the following values depending on what collection type you want to have:

  • @set: for unordered arrays (meaning retrieving a document with that property several times will yield different results)
  • @list: for ordered arrays (keep in mind that this type of collection takes more space and is more expensive overall)
  • @map: for language based maps (TO BE DOCUMENTED)

For example, imagine that the application handled objects like the following one:

var project = {
    labels: [
        "important"
    ],
    tasks: [
        {
            title: "Do something"
        },
        {
            title: "Then do something else"
        },
    ]
};

The labels property is going to have a collection as a value, but it doesn’t need to keep it’s order. After all, its only valuable information is that it “has” this and that label.

On the other hand, the tasks property needs to keep the order of tasks because it indicates the order of their execution or importance.

The object schema should be extended as follows:

appContext.extendObjectSchema( {
    "labels": {
        "@container": "@set"
    },
    "tasks": {
        "@container": "@list"
    }
} );

Having done that, persisting the resource and then reading it again will yield the expected results.

Data types (@type)

The SDK does a really good job on saving values with the right data type. Strings will be saved as strings, booleans will be saved as booleans, etc. But imagine the following scenario:

  1. Application A and B are developed using the same Carbon application to store their data
  2. Both applications are programmed to understand Project objects with a property “content” that holds a string
  3. Later on complex “content” properties are added to application B. Now app B understands “content” properties that hold objects (with a type, and other properties).

Because application A hasn’t been updated, and both apps are using the same data, app A is probably going to crash whenever it finds a complex “content” property .

To avoid this, it is a good practice to define what type the property is meant to have on each application. This is done by specifying a @type property on the object that describes the property “content”:

appContext.extendObjectSchema( {
    "content": {
        "@type": "string"
    }
} );

If the application A has its object schema set this way it won’t have a problem. The SDK will filter any “content” non string value when retrieving the project.

The most common accepted types are:

  • @id: When the value type is a pointer to another object
  • string
  • boolean
  • integer
  • double
  • float
  • long
  • date
  • dateTime
  • time

All of these types (except @id) are part of the xml schema data types.

IDs (@id)

So far we have solved all of the problems we stated at the start of this document. But there’s another problem lurking around. Imagine the following scenario:

  • Application A and application B want to share data by using the same Carbon app
  • Application A handles objects like the following one:
    var client = {
        name: "Miguel",
        lastName: "Aragón"
    };
  • On the other hand application B was programmed to handle objects like this:
    var client = {
        firstName: "Cody",
        lastName: "Burleson"
    };

These two applications will not be able to integrate because they use a different property for the same purpose. Application A uses name to refer to first names, and application B uses firstName instead.

There are two options: one application renames the property to use the same as the other application (expensive!), or they define the same @id on their own object schema.

The property @id configures the unique id the property will have when saved on the platform. That way even if two or more applications are using different property names for the same “real” property, they can integrate by using a unique id for that property.

In this example the problem would be solved by following these steps:

  • Application A extends its object schema like this:
    appContext.extendObjectSchema( {
        "name": {
            "@id": "lastName",
            "@type": "string"
        }
    } );
  • Application B extends its this way:
    appContext.extendObjectSchema( {
        "lastName": {
            "@id": "lastName",
            "@type": "string"
        }
    } );

With that setup, the SDK will translate the properties when saving or retrieving documents and both of the applications will be using the same standardized name for their properties.

Vocabularies

Storing properties with names like name or title is meant to cause collisions. A Carbon application could use “name” to define people’s first names while another one could use it to define their complete name. Or even worse, they could use it to define the name of a country.

That’s why by default each time a new property is saved to a Carbon app it is saved as a uniform resource identifier (URI). For example, if your JavaScript application saves an object with a property title, the property will actually be saved as something like:

http://localhost:8083/apps/your-app-slug/vocabulary/#title

Let’s decompose that URI:

  • http://: The protocol your Carbon platform is configured with
  • localhost: The address of your Carbon platform
  • :8083: The port the Carbon platform is listening on
  • apps/your-app-slug/: Your Carbon app slug
  • vocabulary/: A document inside of your Carbon app
  • #title: The slug of a fragment inside of the vocabulary/ document

This means that the property is saved using the id of a fragment, inside of a document called vocabulary/from your application.

By doing this, you are able to go and save data about the property itself. You could for example go and add a description to the property by modifying the vocabulary/ document:

let appContext;

// ... retrieve appContext

appContext.get( "vocabulary/" ).then(
    ( [ vocabulary, response ] ) => {
        let title = vocabulary.getFragment( "title" );

        // Add metadata about the title property
        title.description = "The title of a book";

        return vocabulary.save();
    }
).then(
    ( response ) => {
        // ...
    }
).catch( console.error );
import * as App from "carbonldp/App";
import * as HTTP from "carbonldp/HTTP";
import * as PersistedDocument from "carbonldp/PersistedDocument";
import * as PersistedFragment from "carbonldp/PersistedFragment";

let appContext:App.Context;

// ... retrieve appContext

appContext.get( "vocabulary/" ).then(
    ( [ vocabulary, response ]:[ PersistedDocument.Class, HTTP.Response.Class ] ) => {
        let title:PersistedFragment.Class = vocabulary.getFragment( "title" );

        // Add metadata about the title property
        title.description = "The title of a book";

        return vocabulary.save();
    }
).then(
    ( response:HTTP.Response.Class ) => {
        // ...
    }
).catch( console.error );
var appContext;

// ... retrieve appContext

appContext.get( "vocabulary/" ).then(
    function( result ) {
        var vocabulary = result[ 0 ];
        var title = vocabulary.getFragment( "title" );

        // Add metadata about the title property
        title.description = "The title of a book";

        return vocabulary.save();
    }
).then(
    function( response ) {
        // ...
    }
).catch( console.error );
The default document those fragments are saved in is called vocabulary because it collects all of the terms you use in your application. By developing an application you are essentially creating a new vocabulary.If that’s the case you may think: “Well what if I reuse the terms from one application on another one?”. Well, that’s one of the main advantages of building a vocabulary!If you have a Carbon application (application A) with an already established amount of terms, and you want to create a new Carbon app (application B) that understands data from your already existing application you can accomplish it by defining the @id of the property in the object schema:

// In application B
appBContext.extendObjectSchema( {
    "title": {
        "@id": "http://localhost:8083/apps/application-a-slug/vocabulary/#title"
    }
} );

Note: This is only needed if your applications are not sharing a Carbon application.

Sharing vocabularies opens a lot of interesting possibilities. For example, you could create your own vocabulary that only contains terms about social connections. Or one that is only about geo location terms. Later on you could make those vocabularies public so other people could use them and make their data compatible with each other!

Well guess what? People have been doing that for quite some time. There are a lot of well established vocabularies that you can use on your application to make your data worldwide compatible. You can search for these vocabularies using the Linked Open Vocabulary searcher.

It is highly recommended to use existing vocabularies instead of new, custom ones.

Prefixes

If you start using other vocabularies (either from other Carbon apps, or from well established vocabularies), you might find it cumbersome to have to write complete URIs when extending object schemas.

Prefixes come to the rescue! A prefix is a short keyword that can be used as a base of a URI. For example, having the following URI:

http://localhost:8083/apps/application-a-slug/vocabulary/#title

You could define the prefix appA and make it equivalent to http://localhost:8083/apps/application-a-slug/vocabulary/#. That way each time you wanted to use that vocabulary you would only need to write appA:title.

Prefixes can be defined in the object schema by defining a property with a string value. The previous example could be defined like this:

appContext.extendObjectSchema( {
    "appA": "http://localhost:8083/apps/application-a-slug/vocabulary/#",
    "title": {
        "@id": "appA:title",
        "@type": "string"
    },
    "content": {
        "@id": "appA:content",
        "@type": "string"
    }
} );

Object types

There’s a problem left to solve. Imagine an application programmed to handle book objects and blog post objects like the following:

var book = {
    title: "The most awesome book you'll ever read"
};

var blogPost = {
    title: "How jellyfish age"
};

Here the problem lies in that both objects use the same property name, title, but they use it for different purposes. A book title cannot be interpreted as a blog post title, and if you try to differentiate them by extending the object schema you won’t be able to (at least not in the way we have done it so far) :

appContext.extendObjectSchema( {
    "title": {
        "@id": "bookTitle"
    },
    "title": {         // ERROR: Duplicate property name
        "@id": "postTitle"
    }
} );

To make a distinction between them, first you need to tag each object with a different type. Types help you tag objects to treat them differently. With them you can filter a list of objects, apply specific rules depending on their types, make assumptions on the properties they’ll have based on their types, etc.

You can tag objects when saving them as documents:

let appContext;

// ... retrieve app context

appContext.documents.createChild( {
    types: [
        "Book"
    ]
} ).then(
    ( [ book, response ] ) => {
        // ...
    }
).catch( console.error );
import * as App from "carbonldp/App";
import * as HTTP from "carbonldp/HTTP";
import * as PersistedDocument from "carbonldp/PersistedDocument";

let appContext:App.Context;

// ... retrieve app context

appContext.documents.createChild( {
    types: [
        "Book"
    ]
} ).then(
    ( [ book, response ]:[ PersistedDocument.Class, HTTP.Response.Class ] ) => {
        // ...
    }
).catch( console.error );
var appContext;

// ... retrieve app context

appContext.documents.createChild( {
    types: [
        "Book"
    ]
} ).then(
    function( result ) {
        // ...
    }
).catch( console.error );

Or you can add or remove types from persisted resources with the addType and removeType:

let appContext;

// ... retrieve app context

appContext.documents.get( "books/awesome-book/" ).then(
    ( [ book, response ] ) => {
        book.addType( "Book" );
        book.removeType( "UncategorizedObject" ); // Not actually a type, just an example

        return book.save();
    }
).then(
    ( [ book, response ] ) => {
        // ...
    }
).catch( console.error );
import * as App from "carbonldp/App";
import * as HTTP from "carbonldp/HTTP";
import * as PersistedDocument from "carbonldp/PersistedDocument";

let appContext:App.Context;

// ... retrieve app context

appContext.documents.get<Book>( "books/awesome-book/" ).then(
    ( [ book, response ]:[ Book & PersistedDocument.Class, HTTP.Response.Class ] ) => {
        book.addType( "Book" );
        book.removeType( "UncategorizedObject" ); // Not actually a type, just an example

        return book.save<Book>();
    }
).then(
    ( [ book, response ]:[ Book & PersistedDocument.Class, HTTP.Response.Class ] ) => {
        // ...
    }
).catch( console.error );
var appContext:App.Context;

// ... retrieve app context

appContext.documents.get( "books/awesome-book/" ).then(
    function( result ) {
        var book = result[ 0 ];
        book.addType( "Book" );
        book.removeType( "UncategorizedObject" ); // Not actually a type, just an example

        return book.save();
    }
).then(
    function( result ) {
        // ...
    }
).catch( console.error );
Once you tag your objects like that you can extend a type specific object schema by defining the type to be extended as the first parameter of extendObjectSchema:

appContext.extendObjectSchema( "Book", {
    "title": {
        "@id": "bookTitle",
        "@type": "string"
    }
} );
appContext.extendObjectSchema( "BlogPost", {
    "title": {
        "@id": "postTitle",
        "@type": "string"
    }
} );

That way the SDK will treat them as different properties. Extending the object schema without specifying a type is called “extending the GLOBAL object schema”, and specifying a type is called “extending the type’s object schema”.

The following rules apply to type’s object schemas:

  • Any property or prefix defined in the global schema will also be defined in a type schema
  • If a type schema defines a property that the global schema already defined, the type schema property will apply

It is a good practice to define prefixes in the global schema, and properties in a type specific object schema.

Conclusion

The SDK does its best to try to guess the desired outcome of retrieving documents or saving new ones. It tries to parse known data types into native JavaScript types and vice versa. But sometimes there is not enough data for the SDK to correctly guess the expected behavior. To make sure the SDK behaves as expected, a web application can give more information about properties and objects to the SDK through a context’s object schema.

Any context has an object schema that can be extended through the extendObjectSchema method.

When extending object schemas properties can be described by declaring a property with an object value. The following properties can be inside that object:

  • @id: To specify the URI to use for the property
  • @type: To declare what data types the application expects on that property
  • @container: To force the SDK to use a specific container type for the property:
    • @set: Unordered list
    • @list: Ordered list
    • @map: Language based map

Prefixes can be defined by declaring a property with a string value. Prefixes can be used to shorten any URI.