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 an object schema.

Any CarbonLDP object 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.

The platform stores each document as a set of statements (or RDF triples ). These statements together represent 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 name, Project 1 (which is a string)
  • project has the label, critical (which is a string)
  • project has the label, hr (which is a string)
  • project has the label, example (which is a string)
  • project was createdOn, 2016/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 instance 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.

The object schema is stored inside of your carbonldp object, and it applies to all your stored documents.

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


carbonldp.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’t need to understand the specification to be able to use object schemas, but again, the more you know, the better.

Properties inside of the object passed to extendObjectSchema will describe different aspects of the schema your instance 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:


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

Collections (@container)

Properties are assumed to be single-valued by default 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)

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:


carbonldp.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 platform instance to store their data.
    
        // App A Carbon LDP instance
        var carbonldpA = new CarbonLDP( "http://my-carbonldp-instance.com:8083" );
    
        // App B Carbon LDP instance
        var carbonldpB = new CarbonLDP( "http://my-carbonldp-instance.com:8083" );
    
  2. Both applications are programmed to understand Project objects with a property “content” that holds a string
// ... initialize your CarbonLDP objects

let project = {
    labels: [
        "critical", "hr", "example"
    ],

    content: "The content"      // Adding property 'content' to be used in app A and B
};

// Saving it into the shared instance using app A
carbonldpA.documents.$create( "/", project, "my-first-project" );

// Retrieving the document in app B
carbonldpB.documents.$get( "my-first-project/" ).then(
    ( storedProject ) => {

        console.log( project.content === storedProject.content ); // true

    }
).catch( error => console.error( error ) );
import { Response } from "carbonldp/HTTP";
import { Document } from "carbonldp/Document";

// ... initialize your CarbonLDP objects

let project:Project = {
    labels: [
        "critical", "hr", "example"
    ],

    content: "The content"      // Adding property 'content' to be used in app A and B
};

// Saving it into the shared instance using app A
carbonldpA.documents.$create( "/", project, "my-first-project" );

// Retrieving the document in app B
carbonldpB.documents.$get( "my-first-project/" ).then(
    ( storedProject:Document ) => {

        console.log( project.content === storedProject.content ); // true

    }
).catch( error => console.error( error ) );
// ... initialize your CarbonLDP objects

var project = {
    labels: [
        "critical", "hr", "example"
    ],

    content: "The content"      // Adding property 'content' to be used in app A and B
};

// Saving it into the shared instance using app A
carbonldpA.documents.$create( "/", project, "my-first-project" );

// Retrieving the document in app B
carbonldpB.documents.$get( "my-first-project/" ).then(
    function( storedProject ) {

        console.log( project.content === storedProject.content ); // true

    }
).catch( function( error ) { console.error( error ); } );
  • Later on, complex “content” properties are added to application B. Now app B understands “content” properties that hold objects (with other
    properties).

 

// ... initialize your CarbonLDP objects

// Retrieving the document in app B
carbonldpB.documents.$get( "my-first-project/" ).then(
    ( storedProject ) => {

        storedProject.content = {      // Changing property 'content' in app B,
            value: "Project content"
            responsible: [
                "Kathy", "Matt"
            ]
        }

        return storedProject.$save();
    }
).catch( error => console.error( error ) );
import { Response } from "carbonldp/HTTP";
import { Document } from "carbonldp/Document";

// ... initialize your CarbonLDP objects

// Retrieving the document in app B
carbonldpB.documents.$get( "my-first-project/" ).then(
    ( storedProject:Document ) => {

        storedProject.content = {      // Changing property 'content' in app B,
            value: "Project content"
            responsible: [
                "Kathy", "Matt"
            ]
        }

        return storedProject.$save();
    }
).catch( error => console.error( error ) );
// ... initialize your CarbonLDP objects

// Retrieving the document in app B
carbonldpB.documents.$get( "my-first-project/" ).then(
    function( storedProject ) {

        storedProject.content = {      // Changing property 'content' in app B,
            value: "Project content"
            responsible: [
                "Kathy", "Matt"
            ]
        }

        return storedProject.$save();
    }
).catch( function( error ) { console.error( error ); } );

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”:


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

If application A has its object schema defined this way, there won’t be 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 platform instance
  • 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:
    
        carbonldpA.extendObjectSchema( {
            "name": {
                "@id": "firstName",
                "@type": "string"
            }
        } );
    
  • Application B extends its this way:
    
        carbonldpB.extendObjectSchema( {
            "firstName": {
                "@id": "firstName",
                "@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.

Languages (@language)

You can specify the language of the property by using the @language property.

carbonldp.extendObjectSchema( {
        "name": {
            "@id": "countryName",
            "@type": "string",
            "@language": "en" // specifies that the country name is given in English
        }
    } );

You can read the W3C’s Language tags in HTML and XML for more details on how specifying a language works.

Vocabularies

Storing properties with names like name or title is meant to cause collisions. An 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 your platform 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:

http://localhost:8083/vocabularies/main/#title

Let’s decompose that URI:

  • http://: The protocol your platform instance is configured with
  • localhost: The address of your platform instance
  • :8083: The port the platform instance is listening on
  • vocabularies/: The container of all the vocabularies
  • main/: A document inside of your Carbon LDP containing your custom vocabulary
  • #title: The slug of a fragment inside of the vocabularies/main/ document

This means that the property -as any other property- is saved using the id of a fragment inside a document called main/ from your platform instance.

 

Be aware that this fragment will not be created inside the main vocabulary unless you manually create it.

 

By creating the fragment inside the main vocabulary, 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 vocabularies/main/ document, like this:

// ... initialize your CarbonLDP object

carbonldp.documents.$get( "vocabularies/main/" ).then(
    ( mainVocabulary ) => {
        let title = mainVocabulary.$createFragment( "title" );

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

        return mainVocabulary.$save();
    }
).then(
    () => {
        // ...
    }
).catch( error => console.error( error ) );
import { Document } from "carbonldp/Document";
import { TransientFragment } from "carbonldp/Fragment";

// ... initialize your CarbonLDP object

carbonldp.documents.$get( "vocabularies/main/" ).then(
    ( mainVocabulary:Document ) => {

        let title:TransientFragment = mainVocabulary.$createFragment( {

            // Add metadata about the title property
            description: "The title of a book"

        }, "title" ); // The name of the property

        return mainVocabulary.$save();
    }
).then(
    () => {
        // ...
    }
).catch( error => console.error( error ) );
// ... initialize your CarbonLDP object

carbonldp.documents.$get( "vocabularies/main/" ).then(
    function( mainVocabulary ) {
        var title = mainVocabulary.$createFragment( "title" ); // or getFragment if it already exists

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

        return mainVocabulary.$save();
    }
).then(
    function() {
        // ...
    }
).catch( function( error ) { console.error( error ); } );
The default document in which those fragments are being saved is called the main 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 LDP application (application A) with an already established amount of terms, and you want to create a new Carbon LDP 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
carbonldpB.extendObjectSchema( {
    "title": {
        "@id": "http://{platform-A-host}/vocabularies/main/#title"
    }
} );

Note: This is only needed if your applications are not sharing a platform instance.

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.

Along with extendObjectSchema(), Carbon also offers the getObjectSchema() method. If no type is provided, it will return the global object schema, otherwise it will return the schema of the type specified.

carbonldp.getObjectSchema(); // returns the general object schema
carbonldp.getObjectSchema("book") // returns the book object schema

 

Tip: The only way to access the application’s vocabulary at runtime is by using the getObjectSchema().vocab property

Prefixes

If you start using other vocabularies (either from other platform instances, 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://my-carbonldp-instance-a:8083/vocabularies/main/#title

You could define the prefix platformA and make it equivalent to http://my-carbonldp-instance-a:8083/vocabularies/main/#. That way each time you want to use that vocabulary you would only need to write platformA: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:

carbonldpB.extendObjectSchema( {
    "platformA": "http://my-carbonldp-instance-a:8083/vocabularies/main/#",
    "title": {
        "@id": "platformA:title",
        "@type": "string"
    },
    "content": {
        "@id": "platformA: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):

carbonldp.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 carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$create( {
    description: "An empty object with a type",
    types: [
        "Book"
    ]
} ).then(
    ( book ) => {
        // ...
    }
).catch( error => console.error( error ) );
import { Document } from "carbonldp/Document";

let carbonldp:CarbonLDP;

// ... initialize your CarbonLDP object

carbonldp.documents.$create( {
    description: "An empty object with a type",
    types: [
        "Book"
    ]
} ).then(
    ( book:Document ) => {
        // ...
    }
).catch( error => console.error( error ) );
var carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$create( {
    description: "An empty object with a type",
    types: [
        "Book"
    ]
} ).then(
    function( book ) {
        // ...
    }
).catch( function( error ) { console.error( error ); } );

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

let carbonldp;

// ... initialize your CarbonLDP object

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

        return book.$save();
    }
).then(
    ( book ) => {
        // ...
    }
).catch( error => console.error( error ) );
import { Document } from "carbonldp/Document";

let carbonldp:Carbon

// ... initialize your CarbonLDP object

carbonldp.documents.$get<Book>( "books/awesome-book/" ).then(
    ( book:Book & Document ) => {
        book.$addType( "Book" );
        book.$removeType( "UncategorizedObject" ); // Not actually a type, just an example

        return book.$save<Book>();
    }
).then(
    ( book:Book & Document ) => {
        // ...
    }
).catch( error => console.error( error ) );
var carbonldp;

// ... initialize your CarbonLDP object

carbonldp.documents.$get( "books/awesome-book/" ).then(
    function( book ) {
        book.$addType( "Book" );
        book.$removeType( "UncategorizedObject" ); // Not actually a type, just an example

        return book.$save();
    }
).then(
    function( book ) {
        // ...
    }
).catch( function( error ) { console.error( 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:

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

That way the SDK will treat them as different properties.

Learn the difference!

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 the 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 an object schema.

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

The extending of object schemas properties can be described as 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

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