C3 AI Documentation Home

Defining Fields in the C3 AI Type System

Fields help define the structure and data of a Type. Each field has a specific ValueType, referred to as its value type, determining the kind of data it can hold. Fields can also have various attributes, including default values, constraints, and visibility settings, which control how they behave and are accessed within the system.

Adding fields to a Type

To declare a Type with fields, you can define the fields within the Type block. For example, you can enhance the Automobile Type by adding three new fields: model, year, and color.

Type
type Automobile {
    model: string
    year: int
    color: string
}

Each field in a Type is associated with a ValueType, which determines the kind of data it can hold. Understanding the value type of a field is crucial for correctly using and manipulating the field's data.

After defining the fields, create an instance of the Automobile Type:

JavaScript
// Create an instance of the Automobile Type in JavaScript
var auto = Automobile.make({
  model: "Porsche Taycan",
  year: 2024,
  color: "white"
});
console.log(auto.model);
console.log(auto.year);
console.log(auto.color);
Python
# Creating an instance of the Automobile Type in Python
porscheAuto = c3.Automobile(model="Porsche Taycan", year=2024, color="white")
print(auto.model)
print(auto.year)
print(auto.color)

Reference fields

Reference fields point to instances of other Types. For example, an Automobile might reference a Manufacturer Type.

Create a new Type, Manufacturer.c3typ.

Type
type Manufacturer {
  name: string
}
Type
// Updated Automobile Type declaration
type Automobile {
    model: string
    year: int
    color: string
    manufacturer: Manufacturer
}

Create two instances of a Manufacturer.

JavaScript
var porscheManufacturer = Manufacturer.make({
  name: "Porsche AG"
});

var teslaManufacturer = Manufacturer.make({
  name: "Tesla, Inc"
});
JavaScript
var porscheAuto = Automobile.make({
  model: "Porsche Taycan",
  year: 2024,
  color: "white",
  manufacturer: porscheManufacturer
});

var testaAuto = Automobile.make({
  model: "Tesla X",
  year: 2024,
  color: "red",
  manufacturer: teslaManufacturer
});
Python
porsche_manufacturer = c3.Manufacturer(name="Porsche AG")
tesla_manufacturer = c3.Manufacturer(name="Tesla, Inc")
Python
c3.Automobile(model="Porsche Taycan", year=2024, color="white", manufacturer=porsche_manufacturer)
Python
c3.Automobile(model="Tesla X", year=2024, color="red", manufacturer=tesla_manufacturer)

Enumeration fields

Enumeration fields limit the possible values for a field, and offer an easy way to work with sets of related constants.

Create a new Type, AutomobileCategory.c3typ.

Type
enum type AutomobileCategory {
    SPORT_CAR
    SEDAN
    SUV
    TRUCK
}

Include the new enum Type in the Automobile declaration:

Type
// Updated Automobile Type declaration
type Automobile {
    model: string
    year: int
    color: string
    manufacturer: Manufacturer
    automobileCategory: string enum AutomobileCategory
}
JavaScript
var porscheAuto = Automobile.make({
  model: "Porsche Taycan",
  year: 2024,
  color: "white",
  manufacturer: porscheManufacturer,
  automobileCategory: AutomobileCategory.SPORT_CAR
});
Python
c3.Automobile(model="Porsche Taycan", year=2024, color="white", manufacturer=porsche_manufacturer, automobileCategory=c3.AutomobileCategory.SPORT_CAR)

Defaults on fields

You can initialize fields on Types with default values using an assignment syntax. This approach ensures that fields are automatically populated with specified default values upon the creation of new instances, in case those fields are not explicitly set.

To assign a default value to a field, use the equals sign = followed by the default value.

Type
type Automobile {
  // Assigning a default value to a string field
  model: string = "Taycan"

  // Assigning a default value to an integer field
  year: int = 2024

  // Assigning a default value to a boolean field
  isElectric: boolean = true
}

Using the above Automobile Type as an example:

JavaScript
var automobileInstance = Automobile.make({});
JSON
{
  "model" : "undefined",
  "year" : "undefined",
  "isElectric" : "undefined"
}
JavaScript
automobileInstance = Automobile.make({}, true);
JSON
{
  "model" : "Taycan",
  "year" : 2024,
  "isElectric" : true
}

The corresponding code in Python:

Python
automobileInstance = c3.Automobile.make(True)
JSON
{
  "type" : "Automobile",
  "model" : "Taycan",
  "year" : 2024,
  "isElectric" : true
}

Note: The default value is setup only when the object is persisted and seed data would take precedence over this default value.

In addition to using make with a boolean to enable defaults, you can use the following method.

The Obj#withDefaults method lets you add default values to an existing instance of an Obj. This method can also include any nested fields within references if you set includeEmptyRefsWithDefaults to true. By passing a list of defaultFields, you can selectively apply default values only to certain fields.

Note: The default value field is set, even if the field is set to null:

JavaScript
automobileInstance = Automobile.make({ 
    "model": "Taycan",
    "year": null
}).withDefaults();
JSON
{
  "model" : "Taycan",
  "year" : null,
  "isElectric" : true
}
JavaScript
// Apply default values, including empty references with default fields
var autoWithDefaults = automobileInstance.withDefaults(true, ["model", "year"]);
Python
automobileInstanceWithDefaults = automobileInstance.withDefaults(True, ["model", "year"])

The default value is now set again.

JSON
{
  "model" : "Taycan",
  "year" : 2024,
  "isElectric" : true
}

The Obj#unsetField method allows you to remove a field from an Obj instance, which makes it not isFieldSet. This differs from setting a field to null, as the field is essentially removed from the instance, making it uninitialized.

JavaScript
// Unset the year field
var autoWithoutYear = automobileInstance.unsetField("year");
Python
automobileInstanceWithoutYear = automobileInstance.unsetField("year");

If you want to remove fields that were not explicitly set or do not meet specific conditions, use Obj#withoutField. This method works similarly to Obj#unsetField, but also removes the field entirely from the instance so that Obj#isFieldSet returns true.

JavaScript
// Remove the color field entirely
var autoWithoutColor = automobileInstance.withoutField("color");
Python
automobileInstanceWithoutYear = automobileInstance.unsetField("model");

Use the Obj#validateObj method to ensure all required fields are set according to default values and Type requirements. You can use this method to enforce constraints, ensuring the Obj instance meets all necessary requirements. This method validates that all required fields have been explicitly set with appropriate values, and throws an error if any constraints are violated. Note that validation is not automatically performed when creating instances with make(), so you must explicitly call validateObj() when you need to ensure an instance conforms to all Type requirements.

JavaScript
var validatedAuto = automobileInstance.validateObj();

Checking field status

The Obj Type offers methods to determine whether fields are set, missing, or empty. These methods are useful when validating data or dynamically handling optional fields.

The Obj#isFieldSet method checks if a field has a value assigned, either explicitly or via a default. This is useful for determining if a field is present, regardless of whether its value is null.

JavaScript
// Check if the 'model' field is set
var isModelSet = automobileInstance.isFieldSet("model");
console.log(isModelSet); // Output: true or false
Python
isModelSet = automobileInstance.isFieldSet("model");

The Obj#isEmptyObj method checks if all fields of an instance are empty. This can be especially helpful when filtering instances or performing bulk operations on empty objects.

JavaScript
// Check if the automobile instance has all empty fields
var isEmpty = automobileInstance.isEmptyObj();
console.log(isEmpty); // Output: true or false
Python
isEmpty = automobileInstance.isEmptyObj()

Accessing and modifying field values

You can directly access and modify fields within an Obj instance using Obj#fieldValue methods, which offer control over both existing and non-existent fields.

The fieldValue method retrieves the value of a specified field. You can pass defaultToEmpty as true to return an empty value if the field is missing.

JavaScript
// Access the 'year' field value, defaulting to empty if missing
var yearValue = automobileInstance.fieldValue("year", true);
console.log(yearValue); // Output: field value or empty if not set
Python
yearValue = automobileInstance.fieldValue("year", True)

If you need to retrieve a field value nested within other fields, use Obj#fieldValueAtPath. This method supports dot-separated paths to access fields within referenced objects.

JavaScript
// Access the 'manufacturer.name' field of the automobile
var manufacturerName = automobileInstance.fieldValueAtPath("model.name", true);
console.log(manufacturerName); // Output: value or empty if path not found
Python
manufacturerName = automobileInstance.fieldValueAtPath("model.name", True)

Iterating over fields

The Obj Type provides methods for iterating over fields. You can perform operations on all fields or apply filters for greater specificity.

Use Obj#eachFieldValue to execute a function for each non-empty field in an instance. This is especially useful when you need to process or log all populated fields dynamically.

JavaScript
// Log each non-empty field's value
automobileInstance.eachFieldValue((ft, value) => {
    console.log(ft.name + ": " + value);
});
Python
for field, value in vars(automobileInstance).items():
    print(f"{field}: {value}")

The Obj#eachRef method iterates over all referenced objects within an instance. This method enables operations on all referenced objects, such as validating or transforming them.

JavaScript
// Log each referenced object's name field, if available
automobileInstance.eachRef((ft, refObj) => {
    console.log(refObj.fieldValue("name", true));
});

Advanced field operations

The Obj Type supports advanced operations like merging and mapping field values, which allow you to customize objects further and handle complex data scenarios.

The Obj#mergeObj method combines the fields of another object into the current object. You can specify which fields to merge and how conflicts should be handled.

JavaScript
// Merge another automobile instance, with field conflicts resolved based on merge rules
var mergedAuto = automobileInstance.mergeObj(Automobile.make({ "year": 2000 }));

Serialization and deserialization

The Obj Type provides methods for converting instances to and from JSON, XML, and JavaScript object literals, supporting interoperability with external systems and formats.

Use Obj#toJson to convert an instance into a JSON object. This method supports options to include or exclude specific fields.

JavaScript
// Serialize to JSON
var jsonObj = automobileInstance.toJson();
console.log(JSON.stringify(jsonObj));

You can reconstruct an Obj instance from a JSON object using fromJson. This method is helpful for loading saved instances or importing data from external sources.

JavaScript
// Deserialize JSON to create a new Automobile instance
var newAutoInstance = Automobile.fromJson({
  model: "Model S",
  year: 2023,
  isElectric: true
});

Updating field values in immutable objects

Because objects in the C3 AI Type System are immutable, you must use methods that return modified copies of instances when you want to update values. The Obj Type provides several ways to accomplish this:

Use Obj#withField to create a new instance with a single field updated. This method takes the field name, the new value, and a flag indicating whether to convert the value to match the field type.

JavaScript
// Update the 'model' field to a new value, producing a new instance
var updatedAuto = automobileInstance.withField("model", "Model S", false);
console.log(updatedAuto.model); // Output: "Model S"

For nested or referenced fields, use Obj#withFieldAtPath. This method allows you to specify a dot-separated path to a field within referenced objects, which creates a new instance with the updated nested field value.

JavaScript
// Update a nested field (manufacturer name), producing a new instance
var updatedAuto = automobileInstance.withFieldAtPath("manufacturer.name", "Tesla", false, false);
console.log(updatedAuto.manufacturer.name); // Output: "Tesla"
Python
updatedAuto = automobileInstance.withFieldAtPath("manufacturer.name", "GE", False, False)

Adding and removing fields

In addition to updating fields, you might want to add or remove fields from an instance. The withField, withoutField, and unsetField methods allow you to manage fields in immutable objects.

JavaScript
// Add a new field 'mileage' with a value, producing a new instance
var autoWithMileage = automobileInstance.withField("mileage", 15000, false);
console.log(autoWithMileage.mileage); // Output: 15000

// Remove the 'year' field, producing a new instance without it
var autoWithoutYear = automobileInstance.withoutField("year");
Python
autoWithMileage = automobileInstance.withField("mileage", 15000, False)
autoWithoutYear = automobileInstance.withoutField("year")

Constant fields

Constant field values cannot be changed. It is possible to declare a field as a constant using the const keyword with an initializer.

Note that the convention is to use ALL CAPS to name a constant field.

Type
type Automobile {
    SPACES: const string = "\s+"
}

Required fields

The ! modifier specifies that a field is required, meaning it must contain a non-null value. However, it's important to understand how "required" is enforced in the C3 AI Type System:

Understanding "required" fields

The ! modifier has definitional semantics rather than constructive semantics. This means:

  • Definitional: A ! field defines what a valid instance of a Type should look like (example: the field must be non-null in a valid instance).
  • Constructive: NOT automatically enforced when you create instances (you can create instances with missing required fields).

Declaring required fields

You can apply the ! modifier to any value Type:

Type
type Automobile {
  // Required primitive field
  year: !int
  
  // Required reference field
  manufacturer: !Manufacturer
  
  // Required array - validateObj() checks if null or empty, but NOT element contents
  maintenanceRecords: ![MaintenanceRecord]
  
  // Required array elements - NOT validated by validateObj()
  colors: [!string]
  
  // Required map - validateObj() checks if null or empty, but NOT value contents
  specifications: !map<!string, !int>
}

Important: Creating instances without required fields

You can create instances without setting required fields using make():

JavaScript
// This succeeds, even though 'year' is required
var auto = Automobile.make({ manufacturer: porscheManufacturer });
console.log(auto.year); // Output: 0 (default for int)
Python
# This also succeeds in Python
auto = c3.Automobile(manufacturer=porsche_manufacturer)
print(auto.year)  # Output: 0 (default for int)

Why does this work? For primitive Types (int, string, boolean, etc.), unset required fields are automatically initialized to their default values across both JavaScript and Python SDKs (0 for int, empty string for string, false for boolean). For reference Types (other Types), unset required fields are left null. This behavior is consistent regardless of whether you're using client-side or server-side code.

Validating required fields

To enforce that all required fields are properly set, you must explicitly call Obj#validateObj:

JavaScript
// Create an Automobile without required fields
var incompleteAuto = Automobile.make({});

// This will throw an error if any required fields are not properly set
try {
  var validatedAuto = incompleteAuto.validateObj();
} catch (error) {
  console.log("Validation failed:", error.message);
}

// Provide all required fields, then validate
var completeAuto = Automobile.make({
  year: 2024,
  manufacturer: porscheManufacturer
});
var validatedAuto = completeAuto.validateObj(); // Succeeds

The validateObj() method is the actual enforcement point for "required" field semantics. It will fail if:

  • A required reference field is null.
  • A required array (marked with !) is null or empty.
  • A required map (marked with !) is null or empty.

About collection element validation: The ! modifier on an array or map checks only if the collection itself is null or empty—it does NOT validate the individual elements. For example:

  • ![MaintenanceRecord] (required array) - validates that the array is not null/empty, but does not check if elements are null.
  • !map<string, TestPlain> (required map) - validates that the map is not null/empty, but does not check if values are null.
  • [!string] (array of required strings) - has a required modifier on the element type, but this is currently not validated in validateObj().

Important distinction for primitives: Since required primitive fields are automatically initialized to defaults (0, empty string, false, etc.), validateObj() will succeed even if you didn't explicitly set them. This is often unexpected behavior. If you need stricter validation that required primitives were explicitly provided, you may need custom validation logic beyond the default validateObj().

Function parameters with the ! modifier

When a function parameter is marked as required with !, it has similar semantics to fields:

Type
type TransportService {
  // Parameter 'vehicle' is required (must not be null at call time)
  calculateDistance: function(vehicle: !Automobile): double
}

function calculateDistance(vehicle) {
  if (vehicle.year) {
    return vehicle.year * 1000;
  }
  return 0;
}

The ! on a function parameter means:

  • You must pass a non-null argument (passing no argument or null will fail at the call boundary)
  • However, the argument is not validated for having all required fields set. You can pass an object with null required fields as long as the object itself is not null

Example: Passing an object with incomplete required fields

JavaScript
// Create an incomplete Automobile (manufacturer is required but null)
var incompleteAuto = Automobile.make({});
console.log(incompleteAuto.manufacturer); // Output: null or undefined

// This call succeeds because incompleteAuto is not null,
// even though it has null required fields
var result = TransportService.calculateDistance(incompleteAuto);
console.log(result); // Output: 0

// Compare with passing null directly (this fails)
TransportService.calculateDistance(null); // Error: null is not allowed for required parameter

This means the actual validation of required fields depends on your function implementation. The ! modifier only ensures the parameter itself is not null, not that all its fields are properly initialized.

Practical examples: How required fields actually work

To illustrate the semantics of required fields, consider these examples:

Example 1: Creating a Type with a required primitive field

Type
type Automobile {
  year: !int
  color: string
}
JavaScript
// This succeeds, even though 'year' is required
var auto = Automobile.make({color: "blue"});
console.log(auto.year); // Output: 0 (primitive default)

Without calling validateObj(), you can create instances that violate required field constraints. The required field year is automatically initialized to 0 (the default for int). Note that this may not represent a valid automobile year in your domain logic.

Example 2: Creating a Type with a required reference field

Type
entity type Automobile {
  year: !int
  manufacturer: !Manufacturer
}
JavaScript
// This succeeds in memory
var incompleteAuto = Automobile.make({year: 2024});
console.log(incompleteAuto.manufacturer); // Output: null or undefined

// But this fails when persisting
incompleteAuto.create(); // Error: required field 'manufacturer' is not set

Reference types cannot use the primitive default approach, so .make() allows creating instances, but they become invalid when validated or persisted. This asymmetry between primitives and references is important to understand.

Example 3: Using validateObj() to enforce requirements

JavaScript
type MaintenanceSession {
  automobile: !Automobile
  date: !string
  description: string
}

// Create an incomplete instance
var session = MaintenanceSession.make({description: "Oil change"});

// Validation fails because required fields are missing
try {
  session.validateObj(); // Throws error: required fields not set
} catch (error) {
  console.log("Validation failed: automobile and date are required");
}

// Validation succeeds when required fields are set
var validSession = MaintenanceSession.make({
  automobile: Automobile.make({year: 2024, manufacturer: porscheManufacturer}),
  date: "2024-12-12",
  description: "Oil change"
}).validateObj();

Example 4: Key takeaway about required field semantics

The following demonstrates the definitional vs. constructive distinction:

JavaScript
// Define a type with required fields
type Automobile {
  year: !int
  manufacturer: !Manufacturer
}

// Constructively: You CAN create an invalid instance
var invalid = Automobile.make({}); // Succeeds

// Definitionally: But it's not a valid instance of Automobile
var isValid = invalid.validateObj(); // Fails - not valid

// To have a valid instance, all required fields must be set
var valid = Automobile.make({
  year: 2024,
  manufacturer: porscheManufacturer
}).validateObj(); // Succeeds - is valid

Collection fields

Collection fields hold multiple values and can be Arrays, Sets, Maps, or Streams. There are various kinds of collections for different purposes, but all collection fields share some common properties:

  • A collection field contains zero or more elements.
  • The elements in a collection field have an ordering.
  • The elements in the collection field have a value type.

Collections are strongly typed, which means they are associated with a specific data type (for example, a collection of primitive values such as string, int, double, or a collection of a specific Type such as the Type Manufacturer). This means they have sub-structure exposed in their value type.

Collection fields can take the following forms:

  • Array: An ordered collection of values.
    • Example: features: [String]
  • Set: An unordered collection of unique values.
    • Example: uniqueIds: set<int>
  • Map: A collection of key-value pairs.
    • Example: settings: map<string, string>
  • Stream: A read-once sequence of values.
    • Example: eventStream: stream<Event>

Collection fields always declare their element Types. This is represented using parametric Type notation in C3 AI's Domain Specific Language (DSL).

Create a new Type, MaintenanceRecord.c3typ.

Type
type MaintenanceRecord {
    description: string
}

Add a new array field to Automobile, maintenanceRecords.

Type
type Automobile {
    model: string
    year: int
    color: string
    manufacturer: Manufacturer
    automobileCategory: AutomobileCategory
    maintenanceRecords: [MaintenanceRecord]
}
JavaScript
var maintenanceRecords = [
  MaintenanceRecord.make({ description: "Faulty battery!" }),
  MaintenanceRecord.make({ description: "Tire alignment!" })
];

var porscheAuto = Automobile.make({
  model: "Porsche Taycan",
  year: 2024,
  color: "white",
  manufacturer: porscheManufacturer,
  automobileCategory: AutomobileCategory.SPORT_CAR,
  maintenanceRecords: maintenanceRecords
});
Python
maintenance_records = [
    c3.MaintenanceRecord(description="Faulty battery!"),
    c3.MaintenanceRecord(description="Tire alignment!")
]

porscheAuto = c3.Automobile(
    model="Porsche Taycan",
    year=2024,
    color="white",
    manufacturer=porsche_manufacturer,
    automobileCategory=c3.AutomobileCategory.SPORT_CAR,
    maintenanceRecords=maintenance_records
)

You can find more details about each of the C3 AI collections in the C3 AI Type System here;

Note: The C3 AI Type System enforces strict rules about data types, for example, ensuring that collections only contain the correct types of objects. It checks for errors while the program is running and supports this functionality across multiple programming languages (JavaScript and Python).

Was this page helpful?