jacobwilliams / json-fortran

A Modern Fortran JSON API
https://jacobwilliams.github.io/json-fortran/
Other
332 stars 83 forks source link

How to read integer arrays with `null` elements? #517

Closed milancurcic closed 2 years ago

milancurcic commented 2 years ago

This program:

use json_module, only: json_core, json_value

type(json_core) :: json
type(json_value), pointer :: json_val
integer, allocatable :: vec(:)
logical :: found

call json % parse(json_val, '{"a": [1, 2]}')
call json % get(json_val, 'a', vec, found)
print *, found, allocated(vec)

end

produces T T as expected.

However, this program

use json_module, only: json_core, json_value

type(json_core) :: json
type(json_value), pointer :: json_val
integer, allocatable :: vec(:)
logical :: found

call json % parse(json_val, '{"a": [1, null]}')
call json % get(json_val, 'a', vec, found)
print *, found, allocated(vec)

end

produces F F, which I didn't expect. I suspect that the null value in an integer array confuses json-fortran here.

If I declare vec as real, then I get NaN for null elements (also expected), and I can recover my non-null integer values from the intermediate real array. Is this your recommended way for getting integer arrays with null elements, or is there a better way?

As an aside, I suggest json % get to return found as .true. in this scenario, and handle a null in some other way. I know that there's no appropriate integer value for a null, but perhaps -huge(vec) would be OK as long as it's documented.

jacobwilliams commented 2 years ago

There is special logic for null to real (see the null_to_real_mode option in json%initialize(), by default setting null to NaN). I never added anything for integers, so that's why you have the behavior you are seeing. If you left out the found and called json%check_for_errors you would get the error message "Unable to resolve value to integer".

You can always traverse the array yourself if you expect nulls to be in there. It is tedious, but here is a very basic example:

program test

use json_module, IK => json_IK, LK => json_LK, CK => json_CK

implicit none

type(json_core) :: json
type(json_value), pointer :: json_val
integer, allocatable :: vec(:)
logical :: found

! original:
call json % parse(json_val, '{"a": [1, null]}')
call json % get(json_val, 'a', vec, found)
print *, found, allocated(vec)

! alt version:
call json_get_integer_vec_by_path_with_nulls(json, json_val, 'a', vec, found)
print *, found, allocated(vec), vec

contains

subroutine json_get_integer_vec_by_path_with_nulls(json, p, path, vec, found)

    !! returns an integer vector, but with any nulls replaced with -huge

    implicit none

    type(json_core),intent(inout)                    :: json
    type(json_value),pointer,intent(in)              :: p
    character(kind=CK,len=*),intent(in)              :: path
    integer(IK),dimension(:),allocatable,intent(out) :: vec
    logical(LK),intent(out),optional                 :: found

    integer(IK) :: var_type, n_children
    integer :: i !! counter
    type(json_value),pointer :: p_array, p_element
    logical(LK) :: error

    if (json%failed()) return

    call json%get(p, path, vec, found)

    if (present(found)) then
        error = .not. found
    else
        error = json%failed()
    end if

    if (error) then

        call json%clear_exceptions()

        call json%info(p, path=path, found=found, var_type=var_type, n_children=n_children)

        if (found .and. var_type==json_array) then

            call json%get(p, path, p_array) ! pointer to array
            allocate(vec(n_children))
            do i = 1, n_children
                call json%get_child(p_array, i, p_element) ! pointer to an element

                ! if an integer, get value, if null, set "nan" value:
                call json%info(p_element, var_type = var_type)
                select case (var_type)
                case(json_integer)
                    call json%get(p_element, vec(i))
                case(json_null)
                    vec(i) = -huge(vec)
                case default
                    error stop 'not an integer or null'
                end select
            end do

        else
            if (present(found)) found = .false.
        end if

    end if

    end subroutine json_get_integer_vec_by_path_with_nulls

end program test

which prints:

 F F
 T T           1 -2147483647

I could add a similar null_to_integer_mode mode for integers. I'd need to think about making -huge be the default, since it seems less obvious than Null to NaN to me. But maybe it is OK.

milancurcic commented 2 years ago

Thanks. I didn't know about check_for_errors, will start using it. Reading as reals and casting to integers does the job for me.