diff --git a/example/write-read-infer.f90 b/example/write-read-infer.f90
new file mode 100644
index 000000000..f69a49580
--- /dev/null
+++ b/example/write-read-infer.f90
@@ -0,0 +1,70 @@
+! Copyright (c), The Regents of the University of California
+! Terms of use are as specified in LICENSE.txt
+program write_read_infer
+  !! This program demonstrates how to write a neural network to a JSON file,
+  !! read the same network from the written file, query the network object for
+  !! some of its properties, print those properties, and use the network to
+  !! perform inference.
+  use command_line_m, only : command_line_t
+  use inference_engine_m, only : inference_engine_t
+  use string_m, only : string_t
+  use matmul_m, only : matmul_t
+  use step_m, only : step_t
+  use file_m, only : file_t
+  implicit none
+
+  type(string_t) file_name
+  type(command_line_t) command_line
+
+  file_name = string_t(command_line%flag_value("--output-file"))
+
+  if (len(file_name%string())==0) then
+    error stop new_line('a') // new_line('a') // &
+      'Usage: ./build/run-fpm.sh run --example write-read-infer -- --output-file "<file-name>"' 
+  end if
+
+  call write_read_query_infer(file_name)
+
+contains
+
+  subroutine write_read_query_infer(output_file_name)
+    type(string_t), intent(in) :: output_file_name
+    integer i, j
+    integer, parameter :: num_inputs = 2, num_outputs = 1, num_neurons = 3, num_hidden_layers = 2
+    integer, parameter :: identity(*,*,*) = & 
+      reshape([((merge(1,0,i==j), i=1,num_neurons), j=1,num_neurons)], shape=[num_neurons,num_neurons,num_hidden_layers-1])
+    type(inference_engine_t) xor_network, inference_engine
+    type(file_t) json_output_file, json_input_file
+
+    print *, "Constructing an inference_engine_t neural-network object from scratch."
+    xor_network = inference_engine_t( &
+      input_weights = real(reshape([1,0,1,1,0,1], [num_inputs, num_neurons])), &
+      hidden_weights = real(identity), &
+      output_weights = real(reshape([1,-2,1], [num_outputs, num_neurons])), &
+      biases = reshape([0.,-1.99,0., 0.,0.,0.], [num_neurons, num_hidden_layers]), &
+      output_biases = [0.], &
+      inference_strategy = matmul_t() &
+    ) 
+    print *, "Converting an inference_engine_t object to a file_t object."
+    json_output_file = xor_network%to_json()
+
+    print *, "Writing an inference_engine_t object to the file '"//output_file_name%string()//"' in JSON format."
+    call json_output_file%write_lines(output_file_name)
+
+    print *, "Reading an inference_engine_t object from the same JSON file '"//output_file_name%string()//"'."
+    json_input_file = file_t(output_file_name)
+
+    print *, "Constructing a new inference_engine_t object from the parameters read."
+    inference_engine = inference_engine_t(json_input_file, step_t(), matmul_t())
+
+    print *, "Querying the new inference_engine_t object for several properties:"
+    print *, "num_outputs = ", inference_engine%num_outputs()
+    print *, "num_hidden_layers = ", inference_engine%num_hidden_layers()
+    print *, "neurons_per_layer = ", inference_engine%neurons_per_layer()
+
+    print *, "Performing inference:"
+    print *, "inference_engine%infer([0.,1.]) =",inference_engine%infer([0.,1.])
+    print *, "Correct answer for the XOR neural network: ", 1.
+  end subroutine write_read_query_infer
+
+end program
diff --git a/src/file_m.f90 b/src/file_m.f90
new file mode 100644
index 000000000..dae7d2c46
--- /dev/null
+++ b/src/file_m.f90
@@ -0,0 +1,48 @@
+module file_m
+  !! A representation of a file as an object
+  use string_m, only : string_t
+
+  private
+  public :: file_t
+
+  type file_t
+    private
+    type(string_t), allocatable :: lines_(:)
+  contains
+    procedure :: lines
+    procedure :: write_lines
+  end type
+
+  interface file_t
+
+    module function read_lines(file_name) result(file_object)
+      implicit none
+      type(string_t), intent(in) :: file_name
+      type(file_t) file_object
+    end function
+
+    pure module function construct(lines) result(file_object)
+      implicit none
+      type(string_t), intent(in), allocatable :: lines(:)
+      type(file_t) file_object
+    end function
+
+  end interface
+
+  interface
+
+    pure module function lines(self)  result(my_lines)
+      implicit none
+      class(file_t), intent(in) :: self
+      type(string_t), allocatable :: my_lines(:)
+    end function
+
+    impure elemental module subroutine write_lines(self, file_name)
+      implicit none
+      class(file_t), intent(in) :: self
+      type(string_t), intent(in), optional :: file_name
+    end subroutine
+
+  end interface
+
+end module file_m
diff --git a/src/file_s.f90 b/src/file_s.f90
new file mode 100644
index 000000000..1ae4c7c36
--- /dev/null
+++ b/src/file_s.f90
@@ -0,0 +1,107 @@
+submodule(file_m) file_s
+  use iso_fortran_env, only : iostat_end, iostat_eor, output_unit
+  use assert_m, only : assert
+  implicit none
+
+contains
+
+  module procedure construct
+    file_object%lines_ = lines
+  end procedure
+
+  module procedure write_lines
+
+    integer file_unit, io_status, l
+
+    call assert(allocated(self%lines_), "file_t%write_lines: allocated(self%lines_)")
+
+    if (present(file_name)) then
+      open(newunit=file_unit, file=file_name%string(), form='formatted', status='unknown', iostat=io_status, action='write')
+      call assert(io_status==0,"write_lines: io_status==0 after 'open' statement", file_name%string())
+    else
+      file_unit = output_unit
+    end if
+
+    do l = 1, size(self%lines_)
+      write(file_unit, *) self%lines_(l)%string()
+    end do
+
+    if (present(file_name)) close(file_unit)
+  end procedure
+  
+  module procedure read_lines
+
+    integer io_status, file_unit, line_num
+    character(len=:), allocatable :: line
+    integer, parameter :: max_message_length=128
+    character(len=max_message_length) error_message
+    integer, allocatable :: lengths(:)
+
+    open(newunit=file_unit, file=file_name%string(), form='formatted', status='old', iostat=io_status, action='read')
+    call assert(io_status==0,"read_lines: io_status==0 after 'open' statement", file_name%string())
+
+    lengths = line_lengths(file_unit)
+
+    associate(num_lines => size(lengths))
+
+      allocate(file_object%lines_(num_lines))
+  
+      do line_num = 1, num_lines
+        allocate(character(len=lengths(line_num)) :: line)
+        read(file_unit, '(a)', iostat=io_status, iomsg=error_message) line
+        call assert(io_status==0,"read_lines: io_status==0 after line read", error_message)
+        file_object%lines_(line_num) = string_t(line)
+        deallocate(line)
+      end do
+
+    end associate
+
+    close(file_unit)
+
+  contains
+   
+    function line_count(file_unit) result(num_lines)
+      integer, intent(in) :: file_unit
+      integer num_lines
+    
+      rewind(file_unit)
+      num_lines = 0 
+      do  
+        read(file_unit, *, iostat=io_status)
+        if (io_status==iostat_end) exit
+        num_lines = num_lines + 1 
+      end do
+      rewind(file_unit)
+    end function
+
+    function line_lengths(file_unit) result(lengths)
+      integer, intent(in) :: file_unit
+      integer, allocatable ::  lengths(:)
+      integer io_status
+      character(len=1) c
+
+      associate(num_lines => line_count(file_unit))
+
+        allocate(lengths(num_lines), source = 0)
+        rewind(file_unit)
+
+        do line_num = 1, num_lines
+          do
+            read(file_unit, '(a)', advance='no', iostat=io_status, iomsg=error_message) c
+            if (io_status==iostat_eor) exit
+            lengths(line_num) = lengths(line_num) + 1
+          end do
+        end do
+
+        rewind(file_unit)
+  
+      end associate
+    end function
+
+  end procedure
+
+  module procedure lines
+    my_lines = self%lines_
+  end procedure
+
+end submodule file_s
diff --git a/src/inference_engine_m.f90 b/src/inference_engine_m.f90
index 86c26ac06..1cd38be10 100644
--- a/src/inference_engine_m.f90
+++ b/src/inference_engine_m.f90
@@ -5,6 +5,7 @@ module inference_engine_m
   use string_m, only : string_t
   use inference_strategy_m, only : inference_strategy_t
   use activation_strategy_m, only : activation_strategy_t
+  use file_m, only : file_t
   implicit none
 
   private
@@ -22,6 +23,7 @@ module inference_engine_m
     class(inference_strategy_t), allocatable :: inference_strategy_
   contains
     procedure :: read_network
+    procedure :: to_json
     procedure :: write_network
     procedure :: infer
     procedure :: num_inputs
@@ -47,10 +49,24 @@ pure module function construct &
       type(inference_engine_t) inference_engine
     end function
 
+    impure elemental module function from_json(file_, activation_strategy, inference_strategy) result(inference_engine)
+      implicit none
+      type(file_t), intent(in) :: file_
+      class(activation_strategy_t), intent(in), optional :: activation_strategy
+      class(inference_strategy_t), intent(in), optional :: inference_strategy
+      type(inference_engine_t) inference_engine
+    end function
+
   end interface
 
   interface
 
+    impure elemental module function to_json(self) result(json_file)
+      implicit none
+      class(inference_engine_t), intent(in) :: self
+      type(file_t) json_file
+    end function
+
     impure elemental module subroutine read_network(self, file_name, activation_strategy, inference_strategy)
       implicit none
       class(inference_engine_t), intent(out) :: self
diff --git a/src/inference_engine_s.f90 b/src/inference_engine_s.f90
index 50546edf9..c026b6b10 100644
--- a/src/inference_engine_s.f90
+++ b/src/inference_engine_s.f90
@@ -5,6 +5,11 @@
   use intrinsic_array_m, only : intrinsic_array_t
   use matmul_m, only : matmul_t
   use step_m, only : step_t
+  use layer_m, only : layer_t
+  use neuron_m, only : neuron_t
+  use file_m, only : file_t
+  use formats_m, only : separated_values
+  use iso_fortran_env, only : iostat_end
   implicit none
 
 contains
@@ -39,7 +44,8 @@
   end procedure
 
   module procedure subtract
-    call assert(self%conformable_with(rhs), "inference_engine_t%subtract: conformable operands")
+    call assert(self%conformable_with(rhs), "inference_engine_t%subtract: conformable operands", &
+      intrinsic_array_t([shape(self%biases_), shape(rhs%biases_)]))
     
     difference%input_weights_  = self%input_weights_  - rhs%input_weights_ 
     difference%hidden_weights_ = self%hidden_weights_ - rhs%hidden_weights_
@@ -60,7 +66,8 @@ pure subroutine assert_consistent(self)
     call assert(allocated(self%activation_strategy_), "inference_engine%assert_consistent: allocated(self%activation_strategy_)")
 
     associate(allocated_components => &
-      [allocated(self%input_weights_), allocated(self%hidden_weights_), allocated(self%biases_), allocated(self%output_weights_)] &
+      [allocated(self%input_weights_), allocated(self%hidden_weights_), allocated(self%output_weights_), &
+       allocated(self%biases_), allocated(self%output_biases_)] &
     )
       call assert(all(allocated_components), "inference_engine_s(assert_consistent): fully allocated object", &
         intrinsic_array_t(allocated_components))
@@ -228,7 +235,6 @@ function line_length(file_unit) result(length)
       integer length, io_status
       character(len=1) c
 
-      rewind(file_unit)
       io_status = 0
       length = 1
       do 
@@ -236,7 +242,7 @@ function line_length(file_unit) result(length)
         if (io_status/=0) exit
         length = length + 1
       end do
-      rewind(file_unit)
+      backspace(file_unit)
     end function
 
     subroutine read_line_and_count_inputs(file_unit, line, input_count)
@@ -441,4 +447,170 @@ subroutine count_outputs(file_unit, buffer_size, num_hidden_layers, output_count
 
   end procedure read_network
 
+  module procedure to_json
+
+    type(string_t), allocatable :: lines(:)
+    integer layer, neuron, line
+    integer, parameter :: characters_per_value=17
+    character(len=:), allocatable :: comma_separated_values, csv_format
+    character(len=17) :: single_value
+    integer, parameter :: &
+      outer_object_braces = 2, hidden_layer_outer_brackets = 2, lines_per_neuron = 4, inner_brackets_per_layer  = 2, &
+      output_layer_brackets = 2, comma = 1
+
+    call self%write_network(string_t("starting-to_json.txt"))
+
+    csv_format = separated_values(separator=",", mold=[real::])
+
+    associate(num_hidden_layers => self%num_hidden_layers(),  neurons_per_layer => self%neurons_per_layer(), &
+      num_outputs => self%num_outputs(), num_inputs => self%num_inputs())
+      associate(num_lines => &
+        outer_object_braces + hidden_layer_outer_brackets &
+        + (num_hidden_layers + 1)*(inner_brackets_per_layer + neurons_per_layer*lines_per_neuron) &
+        + output_layer_brackets + num_outputs*lines_per_neuron &
+      )
+        allocate(lines(num_lines))
+
+        line = 1
+        lines(line) = string_t('{')
+
+        line = line + 1
+        lines(line) = string_t('     "hidden_layers": [')
+
+        layer = 1 
+        line = line + 1
+        lines(line) = string_t('         [')
+        do neuron = 1, neurons_per_layer
+          line = line + 1
+          lines(line) = string_t('             {')
+          line = line + 1
+          allocate(character(len=num_inputs*(characters_per_value+1)-1)::comma_separated_values)
+          write(comma_separated_values, fmt = csv_format) self%input_weights_(:,neuron)
+          lines(line) = string_t('                "weights": [' // trim(comma_separated_values) // '],')
+          deallocate(comma_separated_values)
+          line = line + 1
+          write(single_value, fmt = csv_format) self%biases_(neuron,layer)
+          lines(line) = string_t('                 "bias": ' // trim(single_value))
+          line = line + 1
+          lines(line) = string_t("             }" // trim(merge(' ',',',neuron==neurons_per_layer)))
+        end do
+        line = line + 1
+        lines(line) = string_t(trim(merge("         ],", "         ] ", line/=num_hidden_layers + 1)))
+
+        do layer = 1, num_hidden_layers
+          line = line + 1
+          lines(line) = string_t('         [')
+          do neuron = 1, neurons_per_layer
+            line = line + 1
+            lines(line) = string_t('             {')
+            line = line + 1
+            allocate(character(len=neurons_per_layer*(characters_per_value+1)-1)::comma_separated_values)
+            write(comma_separated_values, fmt = csv_format) self%hidden_weights_(:, neuron, layer)
+            lines(line) = string_t('                "weights": [' // trim(comma_separated_values) // '],')
+            deallocate(comma_separated_values)
+            line = line + 1
+            write(single_value, fmt = csv_format) self%biases_(neuron,layer+1)
+            lines(line) = string_t('                 "bias": ' // trim(single_value))
+            line = line + 1
+            lines(line) = string_t("             }" // trim(merge(' ',',',neuron==neurons_per_layer)))
+          end do
+          line = line + 1
+          lines(line) = string_t("         ]")
+        end do
+
+        line = line + 1
+        lines(line) = string_t("     ],")
+
+        line = line + 1
+        lines(line) = string_t('     "output_layer": [')
+
+        do neuron = 1, num_outputs
+          line = line + 1
+          lines(line) = string_t('             {')
+          line = line + 1
+          allocate(character(len=neurons_per_layer*(characters_per_value+1)-1)::comma_separated_values)
+          write(comma_separated_values, fmt = csv_format) self%output_weights_(neuron,:)
+          lines(line) = string_t('                "weights": [' // trim(comma_separated_values) // '],')
+          deallocate(comma_separated_values)
+          line = line + 1
+          write(single_value, fmt = csv_format) self%output_biases_(neuron)
+          lines(line) = string_t('                 "bias": ' // trim(single_value))
+          line = line + 1
+          lines(line) = string_t("             }")
+        end do
+
+        line = line + 1
+        lines(line) = string_t('     ]')
+
+        line = line + 1
+        lines(line) = string_t('}')
+
+        call assert(line == num_lines, "inference_engine_t%to_json: all lines defined", intrinsic_array_t([num_lines, line]))
+      end associate
+    end associate
+
+    json_file = file_t(lines)
+
+  end procedure to_json
+
+  module procedure from_json
+
+    type(string_t), allocatable :: lines(:)
+    type(layer_t) hidden_layers
+    type(neuron_t) output_neuron
+    
+    lines = file_%lines()
+
+    call assert(adjustl(lines(1)%string())=="{", "from_json: outermost object start", lines(1)%string())
+    call assert(adjustl(lines(2)%string())=='"hidden_layers": [', "from_json: hidden layers array start", lines(2)%string())
+
+    block 
+       integer, parameter :: first_layer_line=3, lines_per_neuron=4, bracket_lines_per_layer=2
+       character(len=:), allocatable :: output_layer_line
+       
+       hidden_layers = layer_t(lines, start=first_layer_line)
+
+       associate( output_layer_line_number => first_layer_line + lines_per_neuron*sum(hidden_layers%count_neurons()) &
+         + bracket_lines_per_layer*hidden_layers%count_layers() + 1)
+
+         output_layer_line = lines(output_layer_line_number)%string()
+         call assert(adjustl(output_layer_line)=='"output_layer": [', "from_json: hidden layers array start", output_layer_line)
+
+         output_neuron = neuron_t(lines, start=output_layer_line_number + 1)
+       end associate
+    end block
+
+    inference_engine%input_weights_ = hidden_layers%input_weights()
+    call assert(hidden_layers%next_allocated(), "inference_engine_t%from_json: next layer exists")
+
+    block 
+      type(layer_t), pointer :: next_layer
+
+      next_layer => hidden_layers%next_pointer()
+      inference_engine%hidden_weights_ = next_layer%hidden_weights()
+    end block
+    inference_engine%biases_ = hidden_layers%hidden_biases()
+
+    associate(output_weights => output_neuron%weights())
+      inference_engine%output_weights_ = reshape(output_weights, [1, size(output_weights)])
+      inference_engine%output_biases_ = [output_neuron%bias()]
+    end associate
+
+
+    if (present(activation_strategy)) then
+      inference_engine%activation_strategy_  = activation_strategy
+    else
+      inference_engine%activation_strategy_  = step_t()
+    end if
+ 
+    if (present(inference_strategy)) then
+      inference_engine%inference_strategy_  = inference_strategy
+    else
+      inference_engine%inference_strategy_  = matmul_t()
+    end if
+
+    call assert_consistent(inference_engine)
+
+  end procedure from_json
+
 end submodule inference_engine_s
diff --git a/src/layer_m.f90 b/src/layer_m.f90
new file mode 100644
index 000000000..ce8b9cb24
--- /dev/null
+++ b/src/layer_m.f90
@@ -0,0 +1,91 @@
+! Copyright (c), The Regents of the University of California
+! Terms of use are as specified in LICENSE.txt
+module layer_m
+  use neuron_m, only : neuron_t
+  use string_m, only : string_t
+  implicit none
+
+  private
+  public :: layer_t
+
+  type layer_t
+    !! linked list of layers, each comprised of a linked list of neurons
+    private
+    type(neuron_t) neuron !! linked list of this layer's neurons 
+    type(layer_t), allocatable :: next !! next layer
+  contains
+    procedure :: count_layers
+    procedure :: count_neurons
+    procedure :: input_weights
+    procedure :: hidden_weights
+    procedure :: hidden_biases
+    procedure :: neurons_per_layer
+    procedure :: next_allocated
+    procedure :: next_pointer
+  end type
+
+  interface layer_t
+
+    recursive module function construct(layer_lines, start) result(layer)
+      !! construct a linked list of layer_t objects from an array of JSON-formatted text lines
+      implicit none
+      type(string_t), intent(in) :: layer_lines(:)
+      integer, intent(in) :: start
+      type(layer_t), target :: layer
+    end function
+
+  end interface
+
+  interface
+
+    module function count_layers(layer) result(num_layers)
+      implicit none
+      class(layer_t), intent(in), target :: layer
+      integer num_layers
+    end function
+
+    module function count_neurons(layer) result(neurons_per_layer)
+      implicit none
+      class(layer_t), intent(in), target :: layer
+      integer, allocatable :: neurons_per_layer(:)
+    end function
+
+    module function input_weights(self) result(weights)
+      implicit none
+      class(layer_t), intent(in), target :: self
+      real, allocatable :: weights(:,:)
+    end function
+
+    module function hidden_weights(self) result(weights)
+      implicit none
+      class(layer_t), intent(in), target :: self
+      real, allocatable :: weights(:,:,:)
+    end function
+
+    module function hidden_biases(self) result(biases)
+      implicit none
+      class(layer_t), intent(in), target :: self
+      real, allocatable :: biases(:,:)
+    end function
+
+    module function neurons_per_layer(self) result(num_neurons)
+      implicit none
+      class(layer_t), intent(in), target :: self
+      integer num_neurons
+    end function
+
+    module function next_allocated(self) result(next_is_allocated)
+      implicit none
+      class(layer_t), intent(in) :: self
+      logical next_is_allocated
+    end function
+
+    module function next_pointer(self) result(next_ptr)
+      implicit none
+      class(layer_t), intent(in), target :: self
+      type(layer_t), pointer :: next_ptr
+    end function
+
+  end interface
+
+end module
diff --git a/src/layer_s.f90 b/src/layer_s.f90
new file mode 100644
index 000000000..8115b4923
--- /dev/null
+++ b/src/layer_s.f90
@@ -0,0 +1,213 @@
+! Copyright (c), The Regents of the University of California
+! Terms of use are as specified in LICENSE.txt
+submodule(layer_m) layer_s
+  use assert_m, only : assert
+  use intrinsic_array_m, only : intrinsic_array_t
+  implicit none
+
+contains
+
+  module procedure construct
+
+    type(neuron_t), pointer ::  neuron 
+    integer num_inputs, neurons_in_layer
+    character(len=:), allocatable :: line
+
+    call assert(adjustl(layer_lines(start)%string())=='[', "layer_t construct: layer start", layer_lines(start)%string())
+
+    layer%neuron = neuron_t(layer_lines, start+1)
+    num_inputs = size(layer%neuron%weights())
+
+    neuron => layer%neuron
+    neurons_in_layer = 1
+    do 
+      if (.not. neuron%next_allocated()) exit
+      neuron => neuron%next_pointer()
+      call assert(size(neuron%weights()) == num_inputs, "layer_t construct: constant number of inputs")
+      neurons_in_layer = neurons_in_layer + 1
+    end do
+
+    line = trim(adjustl(layer_lines(start+4*neurons_in_layer+1)%string()))
+    call assert(line(1:1)==']', "read_layer_list: hidden layer end")
+
+    if (line(len(line):len(line)) == ",") layer%next = construct(layer_lines, start+4*neurons_in_layer+2)
+
+  end procedure
+
+  module procedure count_layers
+
+    type(layer_t), pointer :: layer_ptr
+
+    layer_ptr => layer
+    num_layers = 1 
+    do  
+      if (.not. allocated(layer_ptr%next)) exit
+      layer_ptr => layer%next
+      num_layers = num_layers + 1 
+    end do
+
+  end procedure
+
+  module procedure count_neurons
+
+    type(layer_t), pointer :: layer_ptr
+    type(neuron_t), pointer :: neuron_ptr
+    integer num_neurons
+
+    layer_ptr => layer
+
+    allocate(neurons_per_layer(0))
+
+    do  
+      num_neurons = 1 
+      neuron_ptr => layer_ptr%neuron
+      do  
+        if (.not. neuron_ptr%next_allocated()) exit
+        neuron_ptr => neuron_ptr%next_pointer()
+        num_neurons = num_neurons + 1 
+      end do
+      neurons_per_layer = [neurons_per_layer, num_neurons]
+      if (.not. allocated(layer_ptr%next)) exit
+      layer_ptr => layer%next
+    end do
+ 
+  end procedure
+
+  module procedure input_weights
+
+    type(neuron_t), pointer :: neuron
+    integer i
+
+    associate(num_inputs => self%neuron%num_inputs(), neurons_per_layer => self%neurons_per_layer())
+
+      allocate(weights(num_inputs, neurons_per_layer))
+
+      neuron => self%neuron
+      weights(:,1) = neuron%weights()
+
+      do i = 2, neurons_per_layer - 1
+        call assert(neuron%next_allocated(), "layer_t%input_weights: neuron%next_allocated()")
+        neuron => neuron%next_pointer()
+        weights(:,i) = neuron%weights()
+        call assert(neuron%num_inputs() == num_inputs, "layer_t%input_weights: constant number of inputs")
+      end do
+      neuron => neuron%next_pointer()
+      call assert(.not. neuron%next_allocated(), "layer_t%input_weights: .not. neuron%next_allocated()")
+      if (neurons_per_layer /= 1) weights(:,neurons_per_layer) = neuron%weights()
+
+    end associate
+
+  end procedure
+
+  module procedure hidden_weights
+
+    type(neuron_t), pointer :: neuron
+    type(layer_t), pointer :: layer
+    integer n, l
+
+    associate( &
+      num_inputs => self%neuron%num_inputs(), neurons_per_layer => self%neurons_per_layer(), num_layers => self%count_layers())
+
+      allocate(weights(num_inputs, neurons_per_layer, num_layers))
+
+      layer => self
+
+      loop_over_layers: &
+      do l = 1, num_layers
+
+        neuron => layer%neuron
+        weights(:,1,l) = neuron%weights()
+
+        loop_over_neurons: &
+        do n = 2, neurons_per_layer - 1
+          call assert(neuron%next_allocated(), "layer_t%hidden_weights: neuron%next_allocated()")
+          neuron => neuron%next_pointer()
+          weights(:,n,l) = neuron%weights()
+          call assert(neuron%num_inputs() == num_inputs, "layer_t%hidden_weights: constant number of inputs", &
+            intrinsic_array_t([num_inputs, neuron%num_inputs(), l, n]))
+        end do loop_over_neurons
+
+        call assert(neuron%next_allocated(), "layer_t%hidden_weights: neuron%next_allocated()")
+        neuron => neuron%next_pointer()
+        if (neurons_per_layer /= 1) weights(:,neurons_per_layer,l) = neuron%weights() ! avoid redundant assignment
+
+        if (l/=num_layers) then
+          layer => layer%next
+        else
+          call assert(.not. layer%next_allocated(), "layer_t%hidden_weights: .not. layer%next_allocated()")
+        end if
+
+      end do loop_over_Layers
+
+    end associate
+
+  end procedure
+
+  module procedure hidden_biases
+
+    type(neuron_t), pointer :: neuron
+    type(layer_t), pointer :: layer
+    integer n, l
+
+    associate(neurons_per_layer => self%neurons_per_layer(), num_layers => self%count_layers())
+
+      allocate(biases(neurons_per_layer, num_layers))
+
+      layer => self
+
+      loop_over_layers: &
+      do l = 1, num_layers
+
+        neuron => layer%neuron
+        biases(1,l) = neuron%bias()
+
+        loop_over_neurons: &
+        do n = 2, neurons_per_layer - 1
+          call assert(neuron%next_allocated(), "layer_t%hidden_biases: neuron%next_allocated()", intrinsic_array_t([l,n]))
+          neuron => neuron%next_pointer()
+          biases(n,l) = neuron%bias()
+        end do loop_over_neurons
+
+        call assert(neuron%next_allocated(), "layer_t%hidden_biases: neuron%next_allocated()", &
+          intrinsic_array_t([l,neurons_per_layer]))
+        neuron => neuron%next_pointer()
+        call assert(.not. neuron%next_allocated(), "layer_t%hidden_biases: .not. neuron%next_allocated()", &
+          intrinsic_array_t([l,neurons_per_layer]))
+        if (neurons_per_layer /= 1) biases(neurons_per_layer,l) = neuron%bias() ! avoid redundant assignment
+
+        if (l/=num_layers) then
+          layer => layer%next
+        else
+          call assert(.not. layer%next_allocated(), "layer_t%hidden_biases: .not. layer%next_allocated()")
+        end if
+
+      end do loop_over_layers
+
+    end associate
+
+
+  end procedure hidden_biases
+
+  module procedure neurons_per_layer
+
+    type(neuron_t), pointer ::  neuron 
+
+    neuron => self%neuron
+    num_neurons = 1
+    do 
+      if (.not. neuron%next_allocated()) exit
+      neuron => neuron%next_pointer()
+      num_neurons = num_neurons + 1
+    end do
+
+  end procedure
+
+  module procedure next_allocated
+    next_is_allocated = allocated(self%next)
+  end procedure
+
+  module procedure next_pointer
+    next_ptr => self%next
+  end procedure
+
+end submodule layer_s
diff --git a/src/neuron_m.f90 b/src/neuron_m.f90
new file mode 100644
index 000000000..5447bff9f
--- /dev/null
+++ b/src/neuron_m.f90
@@ -0,0 +1,70 @@
+! Copyright (c), The Regents of the University of California
+! Terms of use are as specified in LICENSE.txt
+module neuron_m
+  use string_m, only : string_t
+  implicit none
+
+  private
+  public :: neuron_t
+
+  type neuron_t
+    !! linked list of neurons
+    private
+    real, allocatable :: weights_(:)
+    real bias_
+    type(neuron_t), allocatable :: next
+  contains
+    procedure :: weights
+    procedure :: bias
+    procedure :: next_allocated
+    procedure :: next_pointer
+    procedure :: num_inputs
+  end type
+
+  interface neuron_t
+
+    pure recursive module function construct(neuron_lines, start) result(neuron)
+      !! construct linked list of neuron_t objects from an array of JSON-formatted text lines
+      implicit none
+      type(string_t), intent(in) :: neuron_lines(:)
+      integer, intent(in) :: start
+      type(neuron_t) neuron
+    end function
+
+  end interface
+
+  interface
+
+    module function weights(self) result(my_weights)
+      implicit none
+      class(neuron_t), intent(in) :: self
+      real, allocatable :: my_weights(:)
+    end function
+
+    module function bias(self) result(my_bias)
+      implicit none
+      class(neuron_t), intent(in) :: self
+      real my_bias
+    end function
+
+    module function next_allocated(self) result(next_is_allocated)
+      implicit none
+      class(neuron_t), intent(in) :: self
+      logical next_is_allocated
+    end function
+
+    module function next_pointer(self) result(next_ptr)
+      implicit none
+      class(neuron_t), intent(in), target :: self
+      type(neuron_t), pointer :: next_ptr
+    end function
+
+    pure module function num_inputs(self) result(size_weights)
+      implicit none
+      class(neuron_t), intent(in) :: self
+      integer size_weights
+    end function
+
+  end interface
+
+end module
diff --git a/src/neuron_s.f90 b/src/neuron_s.f90
new file mode 100644
index 000000000..2b25bd7fc
--- /dev/null
+++ b/src/neuron_s.f90
@@ -0,0 +1,64 @@
+! Copyright (c), The Regents of the University of California
+! Terms of use are as specified in LICENSE.txt
+submodule(neuron_m) neuron_s
+  use assert_m, only : assert
+  implicit none
+
+contains
+
+  module procedure construct
+
+    character(len=:), allocatable :: line
+    integer i
+
+    call assert(adjustl(neuron_lines(start)%string())=='{', "read_json: neuron object start", neuron_lines(start)%string())
+
+    line = neuron_lines(start+1)%string()
+    associate(colon => index(line, ":"))
+      call assert(adjustl(line(:colon-1))=='"weights"', "read_json: neuron weights", line)
+      associate(opening_bracket => colon + index(line(colon+1:), "["))
+        associate(closing_bracket => opening_bracket + index(line(opening_bracket+1:), "]"))
+          associate(commas => count("," == [(line(i:i), i=opening_bracket+1,closing_bracket-1)]))
+            associate(num_inputs => commas + 1)
+              allocate(neuron%weights_(num_inputs))
+              read(line(opening_bracket+1:closing_bracket-1), fmt=*) neuron%weights_
+            end associate
+          end associate
+        end associate
+      end associate
+    end associate
+
+    line = neuron_lines(start+2)%string()
+    associate(colon => index(line, ":"))
+      call assert(adjustl(line(:colon-1))=='"bias"', "read_json: neuron bias", line)
+      read(line(colon+1:), fmt=*) neuron%bias_
+    end associate
+
+    line = adjustl(neuron_lines(start+3)%string())
+    call assert(line(1:1)=='}', "read_json: neuron object end", line)
+    line = adjustr(neuron_lines(start+3)%string())
+    if (line(len(line):len(line)) == ",") neuron%next = construct(neuron_lines, start+4)
+
+  end procedure
+
+  module procedure weights
+    my_weights = self%weights_
+  end procedure
+
+  module procedure bias
+    my_bias = self%bias_
+  end procedure
+
+  module procedure next_allocated
+    next_is_allocated = allocated(self%next)
+  end procedure
+
+  module procedure next_pointer
+    next_ptr => self%next
+  end procedure
+
+  module procedure num_inputs
+    size_weights = size(self%weights_)
+  end procedure
+
+end submodule neuron_s
diff --git a/test/inference_engine_test_m.f90 b/test/inference_engine_test_m.f90
index 92bb834a2..e2282ed50 100644
--- a/test/inference_engine_test_m.f90
+++ b/test/inference_engine_test_m.f90
@@ -8,6 +8,7 @@ module inference_engine_test_m
   use inference_engine_m, only : inference_engine_t
   use inference_strategy_m, only : inference_strategy_t
   use matmul_m, only : matmul_t
+  use file_m, only : file_t
   implicit none
 
   private
@@ -35,12 +36,13 @@ function results() result(test_results)
         "mapping (true,false) to true using the default inference strategy", &
         "mapping (false,true) to true using the default inference strategy", &
         "mapping (false,false) to false using the default inference strategy", &
-        "writing and then reading itself to and from a file", &
         "mapping (true,true) to false using `matmul`-based inference strategy", &
         "mapping (true,false) to true using `matmul`-based inference strategy", &
         "mapping (false,true) to true using `matmul`-based inference strategy", &
-        "mapping (false,false) to false using `matmul`-based inference strategy" &
-      ], [xor_truth_table(), write_then_read(), xor_truth_table(matmul_t())] &
+        "mapping (false,false) to false using `matmul`-based inference strategy", &
+        "writing and then reading itself to and from a file", &
+        "converting to and from JSON format" &
+      ], [xor_truth_table(), xor_truth_table(matmul_t()), write_then_read(), convert_to_and_from_json()] &
     )
   end function
 
@@ -70,9 +72,6 @@ function write_then_read() result(test_passes)
     logical, allocatable :: test_passes
 
     type(inference_engine_t) xor_written, xor_read, difference
-    integer i, j
-    integer, parameter :: identity(*,*,*) = reshape([((merge(1,0,i==j), i=1,3), j=1,3)], shape=[3,3,1])
-    integer, parameter :: num_inputs=2, num_outputs=1, nuerons_per_layer=3
 
     xor_written = xor_network()
     call xor_written%write_network(string_t("build/write_then_read_test_specimen"))
@@ -87,6 +86,25 @@ function write_then_read() result(test_passes)
     end block
   end function
 
+  function convert_to_and_from_json() result(test_passes)
+    logical, allocatable :: test_passes
+
+    type(inference_engine_t) xor, xor_from_json
+    type(file_t) xor_file_object
+
+    xor = xor_network()
+    xor_file_object = xor%to_json()
+    xor_from_json = inference_engine_t(xor_file_object)
+
+    block 
+      type(inference_engine_t) difference
+      real, parameter :: tolerance = 1.0E-06
+
+      difference = xor_from_json - xor
+      test_passes = difference%norm() < tolerance
+    end block
+  end function
+
   function xor_truth_table(inference_strategy) result(test_passes)
     logical, allocatable :: test_passes(:)
     class(inference_strategy_t), intent(in), optional :: inference_strategy