Step 5: Object conversions and error handling

Many applications need to convert the internal in-memory representation of objects into other formats, in order to save them in files and databases, send them over networks, or exchange them with other applications that might be written in different languages.

In our case we will store contribution objects into plain text files.

A very common, simple and well-suited format for storing hierarchical data like object trees is JSON. This is the format we are going to use.

Here is an example of a contribution object encoded as JSON:

{"identifier":1, "first_name":"Giovanni", "last_name":"Spiridigliotsky", "random_number":10, "date_time":"2011-09-17T14:33:22"}

We will now write code to convert a contribution object to JSON (this is also called marshalling in computer science), and then write code to convert JSON back to a contribution object (unmarshalling).

The first step is to add command to_JSON in type contribution:

To implement command to_JSON in factory contribution we could write a command similar to the to_string command in a previous chapter, like this:

   command to_JSON
      script
         o_result = """{ "identifier":{{identifier.to_string}}, "first_name":"{{a_first_name}}", "last_name":"{{a_last_name}}", "random_number":{{a_random_number.to_string}}, "date_time":"{{a_date_time}}" }"""
      end
   end

But there is an easier way. Obix's standard library provides a simple service command to convert any object into JSON:

In the above code we use command object_to_JSON of service JSON_converter. This command takes an object of any type as input argument. In our case we provide the contribution object itself which is expressed by the keyword this. Internally this command uses Obix's reflection mechanism to get all attribute values and convert them into a JSON string.


The complete code for type and factory contribution now looks as follows (test script not shown):

type contribution

   attribute identifier type:positive32 end
   attribute first_name type:user_name end
   attribute last_name type:user_name end
   attribute random_number type:random_1_15 end
   attribute date_time type:local_date_time end

   command to_JSON
      out result type:string end
   end

end
factory contribution type:contribution

   command to_string
      script
         o_result = """{{a_first_name}} {{a_last_name}} contributed '{{a_random_number.to_string}}' on {{a_date_time}}"""
      end
   end
	
   command to_JSON
      script
         o_result = se_JSON_converter.object_to_JSON ( this )
      end
   end

   creator create kind:in_all end

end

We should now test whether command to_JSON works correctly. In a previous chapter we created already a test script in factory contribution. This test script tests the factory as a whole. We could add a new test case to this script. A better way, however, is to attach a separate test script to the command script we just created, in order to keep related things closely together. Proceed as follows


The counterpart of converting an object to JSON is to convert JSON back to an object. Because this involves the creation of a new object we will use a creator in factory contribution to achieve this task.

The creator could be defined like this:

creator create_from_JSON
   in JSON type:string end

   out result type:contribution end
     
   script
      // code to create contribution object from JSON
   end
end

This creator takes the JSON representation of a contribution object as input, and the script converts the JSON string to a contribution object which is returned in output argument result.

The above code would be fine if the JSON string provided as input for the creator was always a valid string. However, we can't assume this because the JSON string will come from an external source at runtime (e.g. a file), which means that it can be invalid and it can't be trusted.

This is a very typical example of a potential resource data error at runtime. We know that something can go wrong. Obix supports multiple output arguments and the solution in Obix is to provide a second output argument (typically named error) that reports any errors encountered.

Now there are two possible outcomes after the creator has been called:

  1. The operation succeeded. In this case:

    • output argument result contains the result
    • output argument error is void

  2. The operation failed. In this case:

    • output argument result is void
    • output argument error contains a description of the error

The creator's code now becomes:

creator create_from_JSON
   in JSON type:string end

   out result type:contribution voidable:yes end
   out error type:string_to_object_conversion_error voidable:yes end
   out_check check: i_result =r void xor i_error =r void end
     
   script
      // code to create contribution object from JSON
   end
end

Note the voidable:yes clause we have to add to both output arguments. We also use Contract Programming (Design by Contract) again (in the out_check instruction) to ensure that result or else error is void (exclusive or).

An interesting question appears: What should happen at runtime if the JSON string is invalid? The right answer is of course that the client code which calls the creator should check for any errors and take appropriate actions.

In practice, the same error handling is often appropriate for many or all clients calling the creator. For example, an error message is appended to a log file. In order to relieve all the clients from doing the same thing, the creator can take care of the error handling. However, flexibility is required, because different clients might need different error handling. The solution in Obix is to provide an error handler as input argument to the creator.

For this purpose Obix's standard library provides type system_error_handler which is defined as follows:

type system_error_handler

   command handle_system_error
      in error type:system_error end
   end

end

In case of an error the creator will:

Every client can provide its own specific error handler to the creator (including error handlers that do nothing). In practice, however, it is often appropriate to define a system wide error handler that is used by default if no specific error handler is provided. Therefore a global default error handler is defined in Obix's standard library. The path to it is org.obix.obix_core.system.se_system_utilities.a_default_system_error_handler. By default the global system error handler simply writes the error description to file error.log in the application's root directory. If you want to change this behavior you can create your own specific global error handler and assign it to se_system_utilities.a_default_system_error_handler in your application's initialization code.

As a result the code for the creator's additional input argument is:

in error_handler type:system_error_handler default:se_system_utilities.default_system_error_handler end

The final signature code for the creator is shown below:

creator create_from_JSON
   in JSON type:string end
   in error_handler type:system_error_handler default:se_system_utilities.default_system_error_handler end

   out result type:contribution voidable:yes end
   out error type:string_to_object_conversion_error voidable:yes end
   out_check check: i_result =r void xor i_error =r void end
     
   script
      // code to create contribution object from JSON
   end
end

The above code shows a very common pattern used in Obix whenever a command execution can fail at runtime. This pattern is used systematically in Obix's standard libraries and it is recommended to also use it in applications.

The benefit is that error handling now becomes easy and straightforward:

For more information please refer to Chapter 11, Runtime error handling


In order to reduce the size of code and avoid code duplication, different source code templates are available in the standard library. In our case template system_error_handler_input_argument can be used as a shortcut to define the error_handler input argument, and template result_xor_string_to_object_conversion_error_output can be used to simplify the code for the output arguments. The creator's simplified code becomes:

creator create_from_JSON
   in JSON type:string end
   %system_error_handler_input_argument

   %result_xor_string_to_object_conversion_error_output < result_type: contribution >
     
   script
      // code to create contribution object from JSON
   end
end

What remains to be done is to write the script code to convert the JSON string into a contribution object.

The simplest way to do this is to use se_JSON_converter again. Command JSON_to_object in this service is defined as follows:

command JSON_to_object
   in JSON type:string end
   in type_of_object type:type end
   %system_error_handler_input_argument
		
   %result_xor_system_error_output < any_type >
end

This command takes three input arguments:

Because JSON_to_object can fail, there are also two output arguments. The pattern we used previously for creator create_from_JSON's output is also used here:

The only thing to do in creator create_from_JSON is to call the above command. Insert the following code in file fa_contribution.osc, just after the existing creator:

   creator create_from_JSON
      in JSON type:string end
      %system_error_handler_input_argument
      
      %result_xor_system_error_output < contribution >
      
      script
         var any_type object_from_JSON
         se_JSON_converter.JSON_to_object ( &
            JSON = i_JSON &
            type_of_object = se_reflection.library_repository.type_by_unprefixed_id_string ( "contribution" ) &
            error_handler = i_error_handler ) &
            ( v_object_from_JSON = result &
            o_error = error )

         if o_error =r void then
            // cast 'v_object_from_JSON' (which is declared of type 'any_type') to type 'contribution'
            // 'type_check:no' tells the compiler to skip type checking at compile-time
            o_result = v_object_from_JSON type_check:no
         else
            o_result = void
         end if
      end
   end

Does everything work correctly? To find it out, insert the following test script just before the last end instruction in creator create_from_JSON:

      test
         script
            test i_JSON = """{ "identifier":123, "first_name":"Albert", "last_name":"Newton", "random_number":5, "date_time":"2011-08-27T16:16:30" }"""
            verify v_result.identifier =v 123
            verify v_result.first_name.value =v "Albert"~
            verify v_result.last_name.value =v "Newton"~
            verify v_result.random_number.value =v 5~
            verify v_result.date_time.value =v "2011-08-27T16:16:30"~

            // test with invalid JSON (first brace removed)
            // remark: 'error_handler' is set to 'se_system_utilities.do_nothing_system_error_handler' in order to
            // display only test results on the system console
            test i_JSON = """ "identifier":123, "first_name":"Albert", "last_name":"Newton", "random_number":5, "date_time":"2011-08-27T16:16:30" }""" &
               error_handler = se_system_utilities.do_nothing_system_error_handler
            verify v_result =r void
            verify v_error #r void

            // test with invalid attribute name ('ident' instead of 'identifier')
            test i_JSON = """{ "ident":123, "first_name":"Albert", "last_name":"Newton", "random_number":5, "date_time":"2011-08-27T16:16:30" }""" &
               error_handler = se_system_utilities.do_nothing_system_error_handler
            verify v_result =r void
            verify v_error #r void

            // test with invalid attribute value ('first_name' contains invalid characters)
            test i_JSON = """{ "identifier":123, "first_name":"Albert<>", "last_name":"Newton", "random_number":5, "date_time":"2011-08-27T16:16:30" }""" &
               error_handler = se_system_utilities.do_nothing_system_error_handler
            verify v_result =r void
            verify v_error #r void
         end
      end

Run the system commands compile_and_build and run_tests, and look at the test results.

[Note]Note
If you are tired of manually executing the system command compile_and_build each time before excuting run_tests then you can create your own system command to automatically call compile_and_build and then run_tests. In Windows, for example, you can create compile_build_and_test.bat with the following content:
@echo off

call compile_and_build
if errorlevel 1 goto end

call run_tests

:end